mirror of
https://github.com/valitydev/salt.git
synced 2024-11-08 09:23:56 +00:00
Merge branch 'highstate'
This commit is contained in:
commit
3f32e1d3e5
30
conf/master
30
conf/master
@ -43,6 +43,15 @@
|
||||
# public keys from the minions
|
||||
#auto_accept: False
|
||||
|
||||
##### State System settings #####
|
||||
##########################################
|
||||
# The state system uses a "top" file to tell the minions what environment to
|
||||
# use and what modules to use. The state_top file is defined relative to the
|
||||
# root of the base environment
|
||||
#state_top: top.yml
|
||||
#
|
||||
# The renderer to use on the minions to render the state data
|
||||
#renderer: yaml_jinja
|
||||
|
||||
##### File Server settings #####
|
||||
##########################################
|
||||
@ -50,8 +59,25 @@
|
||||
# minions. This file server is built into the master daemon and does not
|
||||
# require a dedicated port.
|
||||
|
||||
# The file root is the root directory exposed by the file server
|
||||
#file_root: /srv/salt
|
||||
# The file server works on environments passed to the master, each environment
|
||||
# can have multiple root directories, the subdirectories in the multiple file
|
||||
# roots cannot match, otherwise the downloaded files will not be able to be
|
||||
# reliably ensured. A base environment is required to house the top file
|
||||
# Example:
|
||||
# file_roots:
|
||||
# base:
|
||||
# - /srv/salt/
|
||||
# dev:
|
||||
# - /srv/salt/dev/services
|
||||
# - /srv/salt/dev/states
|
||||
# prod:
|
||||
# - /srv/salt/prod/services
|
||||
# - /srv/salt/prod/states
|
||||
#
|
||||
# Default:
|
||||
#file_roots:
|
||||
# base:
|
||||
# - /srv/salt
|
||||
|
||||
# The hash_type is the hash to use when discovering the hash of a file on
|
||||
# the master server, the default is md5, but sha1, sha224, sha256, sha384
|
||||
|
@ -71,7 +71,6 @@ class Master(object):
|
||||
for name, level in self.opts['log_granular_levels'].iteritems():
|
||||
salt.log.set_logger_level(name, level)
|
||||
import logging
|
||||
self.opts['logger'] = logging.getLogger('salt.stop-using-me')
|
||||
verify_env([os.path.join(self.opts['pki_dir'], 'minions'),
|
||||
os.path.join(self.opts['pki_dir'], 'minions_pre'),
|
||||
os.path.join(self.opts['cachedir'], 'jobs'),
|
||||
@ -139,7 +138,6 @@ class Minion(object):
|
||||
salt.log.set_logger_level(name, level)
|
||||
|
||||
import logging
|
||||
self.opts['logger'] = logging.getLogger('salt.stop-using-me')
|
||||
|
||||
verify_env([self.opts['pki_dir'], self.opts['cachedir']])
|
||||
if self.cli['daemon']:
|
||||
|
@ -70,12 +70,16 @@ def master_config(path):
|
||||
'keep_jobs': 24,
|
||||
'pki_dir': '/etc/salt/pki',
|
||||
'cachedir': '/var/cache/salt',
|
||||
'file_root': '/srv/salt',
|
||||
'file_roots': {
|
||||
'base': ['/srv/salt'],
|
||||
},
|
||||
'file_buffer_size': 1048576,
|
||||
'hash_type': 'md5',
|
||||
'conf_file': path,
|
||||
'open_mode': False,
|
||||
'auto_accept': False,
|
||||
'renderer': 'yaml_jinja',
|
||||
'state_top': 'top.yml',
|
||||
'log_file': '/var/log/salt/master',
|
||||
'log_level': 'warning',
|
||||
'log_granular_levels': {},
|
||||
|
@ -325,34 +325,54 @@ class MWorker(multiprocessing.Process):
|
||||
self._send_cluster()
|
||||
return ret
|
||||
|
||||
def _find_file(self, path, env='base'):
|
||||
'''
|
||||
Search the environment for the relative path
|
||||
'''
|
||||
fnd = {'path': '',
|
||||
'rel': ''}
|
||||
if not self.opts['file_roots'].has_key(env):
|
||||
return fnd
|
||||
for root in self.opts['file_roots'][env]:
|
||||
full = os.path.join(root, path)
|
||||
if os.path.isfile(full):
|
||||
fnd['path'] = full
|
||||
fnd['rel'] = path
|
||||
return fnd
|
||||
return fnd
|
||||
|
||||
def _serve_file(self, load):
|
||||
'''
|
||||
Return a chunk from a file based on the data received
|
||||
'''
|
||||
if not load.has_key('path') or not load.has_key('loc'):
|
||||
return False
|
||||
path = load['path']
|
||||
if path.startswith('/'):
|
||||
path = load['path'][1:]
|
||||
path = os.path.join(self.opts['file_root'], path)
|
||||
if not os.path.isfile(path):
|
||||
return ''
|
||||
fn_ = open(path, 'rb')
|
||||
ret = {'data': '',
|
||||
'dest': ''}
|
||||
if not load.has_key('path')\
|
||||
or not load.has_key('loc')\
|
||||
or not load.has_key('env'):
|
||||
return self.crypticle.dumps(ret)
|
||||
fnd = self._find_file(load['path'], load['env'])
|
||||
if not fnd['path']:
|
||||
return self.crypticle.dumps(ret)
|
||||
ret['dest'] = fnd['rel']
|
||||
fn_ = open(fnd['path'], 'rb')
|
||||
fn_.seek(load['loc'])
|
||||
return self.crypticle.dumps(fn_.read(self.opts['file_buffer_size']))
|
||||
ret['data'] = fn_.read(self.opts['file_buffer_size'])
|
||||
return self.crypticle.dumps(ret)
|
||||
|
||||
def _file_hash(self, load):
|
||||
'''
|
||||
Return a file hash, the hash type is set in the master config file
|
||||
'''
|
||||
if not load.has_key('path'):
|
||||
return False
|
||||
path = os.path.join(self.opts['file_root'], load['path'])
|
||||
if not os.path.isfile(path):
|
||||
if not load.has_key('path')\
|
||||
or not load.has_key('env'):
|
||||
return False
|
||||
path = self._find_file(load['path'], load['env'])
|
||||
if not path:
|
||||
return self.crypticle.dumps('')
|
||||
ret = {}
|
||||
ret['hsum'] = getattr(hashlib, self.opts['hash_type'])(open(path,
|
||||
'rb').read()).hexdigest()
|
||||
ret['hsum'] = getattr(hashlib, self.opts['hash_type'])(
|
||||
open(path, 'rb').read()).hexdigest()
|
||||
ret['hash_type'] = self.opts['hash_type']
|
||||
return self.crypticle.dumps(ret)
|
||||
|
||||
|
183
salt/minion.py
183
salt/minion.py
@ -49,6 +49,7 @@ class Minion(object):
|
||||
self.opts = opts
|
||||
self.mod_opts = self.__prep_mod_opts()
|
||||
self.functions, self.returners = self.__load_modules()
|
||||
self.matcher = Matcher(self.opts, self.functions)
|
||||
self.authenticate()
|
||||
|
||||
def __prep_mod_opts(self):
|
||||
@ -98,10 +99,10 @@ class Minion(object):
|
||||
return
|
||||
# Verify that the publication applies to this minion
|
||||
if data.has_key('tgt_type'):
|
||||
if not getattr(self, '_' + data['tgt_type'] + '_match')(data['tgt']):
|
||||
if not getattr(self.matcher, data['tgt_type'] + '_match')(data['tgt']):
|
||||
return
|
||||
else:
|
||||
if not self._glob_match(data['tgt']):
|
||||
if not self.matcher.glob_match(data['tgt']):
|
||||
return
|
||||
self._handle_decoded_payload(data)
|
||||
|
||||
@ -140,46 +141,6 @@ class Minion(object):
|
||||
target=lambda: self._thread_return(data)
|
||||
).start()
|
||||
|
||||
def _glob_match(self, tgt):
|
||||
'''
|
||||
Returns true if the passed glob matches the id
|
||||
'''
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
cwd = os.getcwd()
|
||||
os.chdir(tmp_dir)
|
||||
open(self.opts['id'], 'w+').write('salt')
|
||||
ret = bool(glob.glob(tgt))
|
||||
os.chdir(cwd)
|
||||
shutil.rmtree(tmp_dir)
|
||||
return ret
|
||||
|
||||
def _pcre_match(self, tgt):
|
||||
'''
|
||||
Returns true if the passed pcre regex matches
|
||||
'''
|
||||
return bool(re.match(tgt, self.opts['id']))
|
||||
|
||||
def _list_match(self, tgt):
|
||||
'''
|
||||
Determines if this host is on the list
|
||||
'''
|
||||
return bool(tgt.count(self.opts['id']))
|
||||
|
||||
def _grain_match(self, tgt):
|
||||
'''
|
||||
Reads in the grains regular expression match
|
||||
'''
|
||||
comps = tgt.split(':')
|
||||
return bool(re.match(comps[1], self.opts['grains'][comps[0]]))
|
||||
|
||||
def _exsel_match(self, tgt):
|
||||
'''
|
||||
Runs a function and return the exit code
|
||||
'''
|
||||
if not self.functions.has_key(tgt):
|
||||
return False
|
||||
return(self.functions[tgt]())
|
||||
|
||||
def _thread_return(self, data):
|
||||
'''
|
||||
This method should be used as a threading target, start the actual
|
||||
@ -297,6 +258,76 @@ class Minion(object):
|
||||
payload = socket.recv_pyobj()
|
||||
self._handle_payload(payload)
|
||||
|
||||
|
||||
class Matcher(object):
|
||||
'''
|
||||
Use to return the value for matching calls from the master
|
||||
'''
|
||||
def __init__(self, opts, functions=None):
|
||||
self.opts = opts
|
||||
if not functions:
|
||||
functions = salt.loader.minion_mods(self.opts)
|
||||
else:
|
||||
self.functions = functions
|
||||
|
||||
|
||||
def confirm_top(self, data):
|
||||
'''
|
||||
Takes the data passed to a top file environment and determines if the
|
||||
data matches this minion
|
||||
'''
|
||||
matcher = 'glob'
|
||||
for item in data:
|
||||
if type(item) == type(dict()):
|
||||
if item.has_key('match'):
|
||||
matcher = item['match']
|
||||
if hasattr(self, matcher + '_match'):
|
||||
return getattr(self, matcher + '_match')
|
||||
else:
|
||||
log.error('Attempting to match with unknown matcher: %s', matcher)
|
||||
return False
|
||||
|
||||
def glob_match(self, tgt):
|
||||
'''
|
||||
Returns true if the passed glob matches the id
|
||||
'''
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
cwd = os.getcwd()
|
||||
os.chdir(tmp_dir)
|
||||
open(self.opts['id'], 'w+').write('salt')
|
||||
ret = bool(glob.glob(tgt))
|
||||
os.chdir(cwd)
|
||||
shutil.rmtree(tmp_dir)
|
||||
return ret
|
||||
|
||||
def pcre_match(self, tgt):
|
||||
'''
|
||||
Returns true if the passed pcre regex matches
|
||||
'''
|
||||
return bool(re.match(tgt, self.opts['id']))
|
||||
|
||||
def list_match(self, tgt):
|
||||
'''
|
||||
Determines if this host is on the list
|
||||
'''
|
||||
return bool(tgt.count(self.opts['id']))
|
||||
|
||||
def grain_match(self, tgt):
|
||||
'''
|
||||
Reads in the grains regular expression match
|
||||
'''
|
||||
comps = tgt.split(':')
|
||||
return bool(re.match(comps[1], self.opts['grains'][comps[0]]))
|
||||
|
||||
def exsel_match(self, tgt):
|
||||
'''
|
||||
Runs a function and return the exit code
|
||||
'''
|
||||
if not self.functions.has_key(tgt):
|
||||
return False
|
||||
return(self.functions[tgt]())
|
||||
|
||||
|
||||
class FileClient(object):
|
||||
'''
|
||||
Interact with the salt master file server.
|
||||
@ -319,16 +350,19 @@ class FileClient(object):
|
||||
'''
|
||||
Make sure that this path is intended for the salt master and trim it
|
||||
'''
|
||||
print path
|
||||
if not path.startswith('salt://'):
|
||||
raise MinionError('Unsupported path')
|
||||
return path[:7]
|
||||
return path[7:]
|
||||
|
||||
def get_file(self, path, dest, makedirs=False):
|
||||
def get_file(self, path, dest='', makedirs=False, env='base'):
|
||||
'''
|
||||
Get a single file from the salt-master
|
||||
'''
|
||||
path = self._check_proto(path)
|
||||
payload = {'enc': 'aes'}
|
||||
fn_ = None
|
||||
if dest:
|
||||
destdir = os.path.dirname(dest)
|
||||
if not os.path.isdir(destdir):
|
||||
if makedirs:
|
||||
@ -337,36 +371,49 @@ class FileClient(object):
|
||||
return False
|
||||
fn_ = open(dest, 'w+')
|
||||
load = {'path': path,
|
||||
'env': env,
|
||||
'cmd': '_serve_file'}
|
||||
while True:
|
||||
if not fn_:
|
||||
load['loc'] = 0
|
||||
else:
|
||||
load['loc'] = fn_.tell()
|
||||
payload['load'] = self.crypticle.dumps(load)
|
||||
payload['load'] = self.auth.crypticle.dumps(load)
|
||||
self.socket.send_pyobj(payload)
|
||||
data = self.auth.crypticle.loads(self.socket.recv_pyobj())
|
||||
if not data:
|
||||
if not data['data']:
|
||||
break
|
||||
fn_.write(data)
|
||||
if not fn_:
|
||||
dest = os.path.join(
|
||||
self.opts['cachedir'],
|
||||
'files',
|
||||
data['dest']
|
||||
)
|
||||
destdir = os.path.dirname(dest)
|
||||
if not os.path.isdir(destdir):
|
||||
os.makedirs(destdir)
|
||||
fn_ = open(dest, 'w+')
|
||||
fn_.write(data['data'])
|
||||
return dest
|
||||
|
||||
def cache_file(self, path):
|
||||
def cache_file(self, path, env='base'):
|
||||
'''
|
||||
Pull a file down from the file server and store it in the minion file
|
||||
cache
|
||||
'''
|
||||
dest = os.path.join(self.opts['cachedir'], 'files', path)
|
||||
return self.get_file(path, dest, True)
|
||||
return self.get_file(path, '', True, env)
|
||||
|
||||
def cache_files(self, paths):
|
||||
def cache_files(self, paths, env='base'):
|
||||
'''
|
||||
Download a list of files stored on the master and put them in the minion
|
||||
file cache
|
||||
'''
|
||||
ret = []
|
||||
for path in paths:
|
||||
ret.append(self.cache_file(path))
|
||||
ret.append(self.cache_file(path, env))
|
||||
return ret
|
||||
|
||||
def hash_file(self, path):
|
||||
def hash_file(self, path, env='base'):
|
||||
'''
|
||||
Return the hash of a file, to get the hash of a file on the
|
||||
salt master file server prepend the path with salt://<file on server>
|
||||
@ -375,7 +422,35 @@ class FileClient(object):
|
||||
path = self._check_proto(path)
|
||||
payload = {'enc': 'aes'}
|
||||
load = {'path': path,
|
||||
'env': env,
|
||||
'cmd': '_file_hash'}
|
||||
payload['load'] = auth.crypticle.dumps(load)
|
||||
payload['load'] = self.auth.crypticle.dumps(load)
|
||||
self.socket.send_pyobj(payload)
|
||||
return self.auth.crypticle.loads(socket.recv_pyobj())
|
||||
|
||||
def get_state(self, sls, env):
|
||||
'''
|
||||
Get a state file from the master and store it in the local minion cache
|
||||
return the location of the file
|
||||
'''
|
||||
if sls.count('.'):
|
||||
sls = sls.replace('.', '/')
|
||||
for path in [
|
||||
'salt://' + sls + '.sls',
|
||||
os.path.join('salt://', sls, 'init.sls')
|
||||
]:
|
||||
dest = self.cache_file(path, env)
|
||||
if dest:
|
||||
return dest
|
||||
return False
|
||||
|
||||
def master_opts(self):
|
||||
'''
|
||||
Return the master opts data
|
||||
'''
|
||||
payload = {'enc': 'aes'}
|
||||
load = {'cmd': '_master_opts'}
|
||||
payload['load'] = self.auth.crypticle.dumps(load)
|
||||
self.socket.send_pyobj(payload)
|
||||
return self.auth.crypticle.loads(self.socket.recv_pyobj())
|
||||
|
||||
|
@ -38,30 +38,30 @@ def recv(files, dest):
|
||||
|
||||
return ret
|
||||
|
||||
def get_file(path, dest):
|
||||
def get_file(path, dest, env='base'):
|
||||
'''
|
||||
Used to get a single file from the salt master
|
||||
'''
|
||||
client = salt.minion.FileClient(__opts__)
|
||||
return client.get_file(path, dest)
|
||||
return client.get_file(path, dest, False, env)
|
||||
|
||||
def cache_files(paths):
|
||||
def cache_files(paths, env='base'):
|
||||
'''
|
||||
Used to gather many files from the master, the gathered files will be
|
||||
saved in the minion cachedir reflective to the paths retrieved from the
|
||||
master.
|
||||
'''
|
||||
client = salt.minion.FileClient(__opts__)
|
||||
return client.cache_files(paths)
|
||||
return client.cache_files(paths, env)
|
||||
|
||||
def cache_file(path):
|
||||
def cache_file(path, env='base'):
|
||||
'''
|
||||
Used to cache a single file in the local salt-master file cache.
|
||||
'''
|
||||
client = salt.minion.FileClient(__opts__)
|
||||
return client.cache_file(path)
|
||||
return client.cache_file(path, env)
|
||||
|
||||
def hash_file(path):
|
||||
def hash_file(path, env='base'):
|
||||
'''
|
||||
Return the hash of a file, to get the hash of a file on the
|
||||
salt master file server prepend the path with salt://<file on server>
|
||||
@ -70,4 +70,4 @@ def hash_file(path):
|
||||
CLI Example:
|
||||
'''
|
||||
client = salt.minion.FileClient(__opts__)
|
||||
return client.hash_file(path)
|
||||
return client.hash_file(path, env)
|
||||
|
@ -50,3 +50,12 @@ def template_str(tem):
|
||||
st_ = salt.state.State(__opts__)
|
||||
return st_.call_template_str(tem)
|
||||
|
||||
def highstate():
|
||||
'''
|
||||
Retrive the state data from the salt master for this minion and execute it
|
||||
|
||||
CLI Example:
|
||||
salt '*' state.highstate
|
||||
'''
|
||||
st_ = salt.state.HighState(__opts__)
|
||||
return st_.call_highstate()
|
||||
|
@ -16,8 +16,14 @@ import os
|
||||
import copy
|
||||
import inspect
|
||||
import tempfile
|
||||
import logging
|
||||
# Import Salt modules
|
||||
import salt.loader
|
||||
import salt.minion
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class StateError(Exception): pass
|
||||
|
||||
class State(object):
|
||||
'''
|
||||
@ -271,3 +277,94 @@ class State(object):
|
||||
if high:
|
||||
return self.call_high(high)
|
||||
return high
|
||||
|
||||
class HighState(object):
|
||||
'''
|
||||
Generate and execute the salt "High State". The High State is the compound
|
||||
state derived from a group of template files stored on the salt master or
|
||||
in a the local cache.
|
||||
'''
|
||||
def __init__(self, opts):
|
||||
self.client = salt.minion.FileClient(opts)
|
||||
self.opts = self.__gen_opts(opts)
|
||||
self.state = State(self.opts)
|
||||
self.matcher = salt.minion.Matcher(self.opts)
|
||||
|
||||
def __gen_opts(self, opts):
|
||||
'''
|
||||
The options used by the High State object are derived from options on
|
||||
the minion and the master, or just the minion if the high state call is
|
||||
entirely local.
|
||||
'''
|
||||
# If the state is intended to be applied locally, then the local opts
|
||||
# should have all of the needed data, otherwise overwrite the local
|
||||
# data items with data from the master
|
||||
if opts.has_key('local_state'):
|
||||
if opts['local_state']:
|
||||
return opts
|
||||
mopts = self.client.master_opts()
|
||||
opts['renderer'] = mopts['renderer']
|
||||
if mopts['state_top'].startswith('salt://'):
|
||||
opts['state_top'] = mopts['state_top']
|
||||
elif mopts['state_top'].startswith('/'):
|
||||
opts['state_top'] = os.path.join('salt://', mopts['state_top'][1:])
|
||||
else:
|
||||
opts['state_top'] = os.path.join('salt://', mopts['state_top'])
|
||||
return opts
|
||||
|
||||
def get_top(self):
|
||||
'''
|
||||
Returns the high data derived from the top file
|
||||
'''
|
||||
top = self.client.cache_file(self.opts['state_top'], 'base')
|
||||
return self.state.compile_template(top)
|
||||
|
||||
def top_matches(self, top):
|
||||
'''
|
||||
Search through the top high data for matches and return the states that
|
||||
this minion needs to execute.
|
||||
|
||||
Returns:
|
||||
{'env': ['state1', 'state2', ...]}
|
||||
'''
|
||||
matches = {}
|
||||
for env, body in top.items():
|
||||
for match, data in body.items():
|
||||
if self.matcher.confirm_top(data):
|
||||
if not matches.has_key(env):
|
||||
matches[env] = []
|
||||
for item in data:
|
||||
if type(item) == type(str()):
|
||||
matches[env].append(item)
|
||||
return matches
|
||||
|
||||
def gather_states(self, matches):
|
||||
'''
|
||||
Gather the template files from the master
|
||||
'''
|
||||
group = []
|
||||
for env, states in matches.items():
|
||||
for sls in states:
|
||||
state = self.client.get_state(sls, env)
|
||||
if state:
|
||||
group.append(state)
|
||||
return group
|
||||
|
||||
def render_highstate(self, group):
|
||||
'''
|
||||
Renders the collection of states into a single highstate data structure
|
||||
'''
|
||||
highstate = {}
|
||||
for sls in group:
|
||||
highstate.update( self.state.compile_template(sls))
|
||||
return highstate
|
||||
|
||||
def call_highstate(self):
|
||||
'''
|
||||
Run the sequence to execute the salt highstate for this minion
|
||||
'''
|
||||
top = self.get_top()
|
||||
matches = self.top_matches(top)
|
||||
group = self.gather_states(matches)
|
||||
high = self.render_highstate(group)
|
||||
return self.state.call_high(high)
|
||||
|
@ -27,8 +27,9 @@ def latest(name):
|
||||
'''
|
||||
Verify that the latest package is installed
|
||||
'''
|
||||
changes = {}
|
||||
version = __salt__['pkg.version'](name)
|
||||
avail = ['pkg.available_version'](name)
|
||||
avail = __salt__['pkg.available_version'](name)
|
||||
if avail > version:
|
||||
changes = __salt__['pkg.install'](name, True)
|
||||
if not changes:
|
||||
|
Loading…
Reference in New Issue
Block a user