diff --git a/salt/client/__init__.py b/salt/client/__init__.py index 717787acb9..2f05fb9389 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -212,9 +212,10 @@ class LocalClient(object): if not pub_data: # Failed to autnenticate, this could be a bunch of things raise EauthAuthenticationError( - 'Failed to authenticate! This could be a number of issues:' - '1: Is this user permitted to execute commands?' - '2: A disk error may have occured, check disk usage and inode usage.' + 'Failed to authenticate! This is most likely because this ' + 'user is not permitted to execute commands, but there is a ' + 'small possibility that a disk error ocurred (check ' + 'disk/inode usage).' ) # Failed to connect to the master and send the pub diff --git a/salt/cloud/clouds/lxc.py b/salt/cloud/clouds/lxc.py index ec2c4ed963..dbeb41e301 100644 --- a/salt/cloud/clouds/lxc.py +++ b/salt/cloud/clouds/lxc.py @@ -153,6 +153,38 @@ def _salt(fun, *args, **kw): rkwargs['timeout'] = timeout rkwargs.setdefault('expr_form', 'list') kwargs.setdefault('expr_form', 'list') + ping_retries = 0 + # the target(s) have environ one minute to respond + # we call 60 ping request, this prevent us + # from blindly send commands to unmatched minions + ping_max_retries = 60 + ping = True + # do not check ping... if we are pinguing + if fun == 'test.ping': + ping_retries = ping_max_retries + 1 + # be sure that the executors are alive + while ping_retries <= ping_max_retries: + try: + if ping_retries > 0: + time.sleep(1) + pings = conn.cmd(tgt=target, + timeout=10, + fun='test.ping') + values = pings.values() + if not values: + ping = False + for v in values: + if v is not True: + ping = False + if not ping: + raise ValueError('Unreachable') + break + except Exception: + ping = False + ping_retries += 1 + log.error('{0} unreachable, retrying'.format(target)) + if not ping: + raise SaltCloudSystemExit('Target {0} unreachable'.format(target)) jid = conn.cmd_async(tgt=target, fun=fun, arg=args, @@ -363,342 +395,38 @@ def create(vm_, call=None): '''Create an lxc Container. This function is idempotent and will try to either provision or finish the provision of an lxc container. + + NOTE: Most of the initialisation code has been moved and merged + with the lxc runner and lxc.init functions ''' - mopts = _master_opts() - if not get_configured_provider(vm_): - return __grains__ = _salt('grains.items') - name = vm_['name'] - if 'minion' not in vm_: - vm_['minion'] = {} - minion = vm_['minion'] - - from_container = vm_.get('from_container', None) - image = vm_.get('image', None) - vgname = vm_.get('vgname', None) - backing = vm_.get('backing', None) - snapshot = vm_.get('snapshot', False) + prov = get_configured_provider(vm_) + if not prov: + return profile = vm_.get('profile', None) - fstype = vm_.get('fstype', None) - dnsservers = vm_.get('dnsservers', []) - lvname = vm_.get('lvname', None) - ip = vm_.get('ip', None) - mac = vm_.get('mac', None) - netmask = vm_.get('netmask', '24') - bridge = vm_.get('bridge', 'lxcbr0') - gateway = vm_.get('gateway', 'auto') - autostart = vm_.get('autostart', True) - if autostart: - autostart = "1" - else: - autostart = "0" - size = vm_.get('size', '10G') - ssh_username = vm_.get('ssh_username', 'user') - sudo = vm_.get('sudo', True) - password = vm_.get('password', 'user') - lxc_conf_unset = vm_.get('lxc_conf_unset', []) - lxc_conf = vm_.get('lxc_conf', []) - stopped = vm_.get('stopped', False) - master = vm_.get('master', None) - script = vm_.get('script', None) - script_args = vm_.get('script_args', None) - users = vm_.get('users', None) - # some backends wont support some parameters - if backing not in ['lvm']: - lvname = vgname = None - if backing in ['dir', 'overlayfs']: - fstype = None - size = None - if backing in ['dir']: - snapshot = False - for k in ['password', - 'ssh_username']: - vm_[k] = locals()[k] - + if not profile: + profile = {} salt.utils.cloud.fire_event( - 'event', - 'starting create', + 'event', 'starting create', 'salt/cloud/{0}/creating'.format(vm_['name']), - { - 'name': vm_['name'], - 'profile': vm_['profile'], - 'provider': vm_['provider'], - }, - transport=__opts__['transport'] - ) - if not dnsservers: - dnsservers = ['8.8.8.8', '4.4.4.4'] - changes = {} - changed = False - ret = {'name': name, - 'changes': changes, - 'result': True, - 'comment': ''} - if not users: - users = ['root'] - if ( - __grains__['os'] in ['Ubuntu'] - and 'ubuntu' not in users - ): - users.append('ubuntu') - if ssh_username not in users: - users.append(ssh_username) - if not users: - users = [] - if not lxc_conf: - lxc_conf = [] - if not lxc_conf_unset: - lxc_conf_unset = [] - if from_container: - method = 'clone' - else: - method = 'create' - if ip is not None: - lxc_conf.append({'lxc.network.ipv4': '{0}/{1}'.format(ip, netmask)}) - if mac is not None: - lxc_conf.append({'lxc.network.hwaddr': mac}) - if gateway is not None: - lxc_conf.append({'lxc.network.ipv4.gateway': gateway}) - if bridge is not None: - lxc_conf.append({'lxc.network.link': bridge}) - lxc_conf.append({'lxc.start.auto': autostart}) - changes['100_creation'] = '' - created = False - cret = {'name': name, 'changes': {}, 'result': True, 'comment': ''} - exists = _salt('lxc.exists', name) - if exists: - cret['comment'] = 'Container already exists' - cret['result'] = True - elif method == 'clone': - oexists = _salt('lxc.exists', from_container) - if not oexists: - cret['result'] = False - cret['comment'] = ( - 'container could not be cloned: {0}, ' - '{1} does not exist'.format(name, from_container)) - else: - nret = _salt('lxc.clone', - name, - orig=from_container, - snapshot=snapshot, - size=size, - backing=backing, - profile=profile) - if nret.get('error', ''): - cret['result'] = False - cret['comment'] = '{0}\n{1}'.format( - nret['error'], 'Container cloning error') - else: - cret['result'] = ( - nret['cloned'] - or 'already exist' in cret.get('comment', '')) - cret['comment'] += 'Container cloned\n' - cret['changes']['status'] = 'cloned' - elif method == 'create': - nret = _salt('lxc.create', - name, - template=image, - profile=profile, - fstype=fstype, - vgname=vgname, - size=size, - lvname=lvname, - backing=backing) - if nret.get('error', ''): - cret['result'] = False - cret['comment'] = nret['error'] - else: - exists = ( - nret['created'] - or 'already exist' in nret.get('comment', '')) - cret['comment'] += 'Container created\n' - cret['changes']['status'] = 'Created' - changes['100_creation'] = cret['comment'] - ret['comment'] = changes['100_creation'] - if not cret['result']: + {'name': vm_['name'], 'profile': vm_['profile'], + 'provider': vm_['provider'], }, + transport=__opts__['transport']) + ret = {'name': vm_['name'], 'changes': {}, 'result': True, 'comment': ''} + if 'pub_key' not in vm_ and 'priv_key' not in vm_: + log.debug('Generating minion keys for {0}'.format(vm_['name'])) + vm_['priv_key'], vm_['pub_key'] = salt.utils.cloud.gen_keys( + salt.config.get_cloud_config_value( + 'keysize', vm_, __opts__)) + # get the minion key pair to distribute back to the container + kwarg = copy.deepcopy(vm_) + kwarg['host'] = prov['target'] + cret = _runner().cmd('lxc.cloud_init', [vm_['name']], kwarg=kwarg) + ret['runner_return'] = cret + if cret['result']: ret['result'] = False - ret['comment'] = cret['comment'] - _checkpoint(ret) - if cret['changes']: - created = changed = True - - # edit lxc conf if any - changes['150_conf'] = '' - cret['result'] = False - cret = _salt('lxc.update_lxc_conf', - name, - lxc_conf=lxc_conf, - lxc_conf_unset=lxc_conf_unset) - if not cret['result']: - ret['result'] = False - ret['comment'] = cret['comment'] - _checkpoint(ret) - if cret['changes']: - changed = True - changes['150_conf'] = 'lxc conf ok' - - # start - changes['200_start'] = 'started' - ret['comment'] = changes['200_start'] - # reboot if conf has changed - cret = _salt('lxc.start', name, restart=changed) - if not cret['result']: - ret['result'] = False - changes['200_start'] = cret['comment'] - ret['comment'] = changes['200_start'] - _checkpoint(ret) - if cret['changes']: - changed = True - - # first time provisionning only, set the default user/password - changes['250_password'] = 'Passwords in place' - ret['comment'] = changes['250_password'] - gid = '/.lxc.{0}.initial_pass'.format(name, False) - lxcret = _salt('lxc.run_cmd', - name, 'test -e {0}'.format(gid), - stdout=False, stderr=False) - if lxcret: - cret = _salt('lxc.set_pass', - name, - password=password, users=users) - changes['250_password'] = 'Password updated' - if not cret['result']: - ret['result'] = False - changes['250_password'] = 'Failed to update passwords' - ret['comment'] = changes['250_password'] - _checkpoint(ret) - try: - lxcret = int( - _salt('lxc.run_cmd', - name, - 'sh -c \'touch "{0}"; ' - 'test -e "{0}";echo ${{?}}\''.format(gid))) - except ValueError: - lxcret = 1 - ret['result'] = not bool(lxcret) - if not cret['result']: - changes['250_password'] = 'Failed to test password file marker' - _checkpoint(ret) - changed = True - - def wait_for_ip(): - ''' - Wait for the IP address to become available - ''' - try: - data = show_instance(vm_['name'], call='full') - except Exception: - data = {'private_ips': [], 'public_ips': []} - ips = data['private_ips'] + data['public_ips'] - if ips: - if ip and ip in ips: - return ip - return ips[0] - time.sleep(1) - return False - ip = salt.utils.cloud.wait_for_fun( - wait_for_ip, - timeout=config.get_cloud_config_value( - 'wait_for_fun_timeout', vm_, __opts__, default=15 * 60)) - changes['300_ipattrib'] = 'Got ip {0}'.format(ip) - if not ip: - changes['300_ipattrib'] = 'Cant get ip' - ret['result'] = False - ret['comment'] = changes['300_ipattrib'] - _checkpoint(ret) - - # set dns servers - changes['350_dns'] = 'DNS in place' - ret['comment'] = changes['350_dns'] - gid = 'lxc.{0}.initial_dns'.format(name, False) - lxcret = _salt('lxc.run_cmd', - name, - 'test -e {0}'.format(gid), - stdout=False, stderr=False,) - if dnsservers and not lxcret: - cret = _salt('lxc.set_dns', - name, - dnsservers=dnsservers) - changes['350_dns'] = 'DNS updated' - ret['comment'] = changes['350_dns'] - if not cret['result']: - ret['result'] = False - changes['350_dns'] = 'DNS provisionning error' - ret['comment'] = changes['350_dns'] - try: - lxcret = int( - _salt('lxc.run_cmd', - name, - 'sh -c \'touch "{0}"; ' - 'test -e "{0}";echo ${{?}}\''.format(gid))) - except ValueError: - lxcret = 1 - ret['result'] = not lxcret - if not cret['result']: - changes['250_password'] = 'Failed to test DNS set marker' - _checkpoint(ret) - changed = True - _checkpoint(ret) - - # provision salt on the fresh container - if 'master' in minion: - changes['400_salt'] = 'This vm is a salt minion' - - def testping(*args): - ping = _salt('test.ping', **{'salt_target': vm_['name']}) - time.sleep(1) - if ping: - return 'OK' - raise Exception('Unresponsive {0}'.format(vm_['name'])) - # if already created, test to ping before bindly go to saltify - # we ping for 1 minute - skip = False - if not created: - ping = salt.utils.cloud.wait_for_fun(testping, timeout=10) - if ping == 'OK': - skip = True - - if not skip: - minion['master_port'] = mopts.get('ret_port', '4506') - vm_['ssh_host'] = ip - vm_['sudo'] = sudo - vm_['sudo_password'] = password - svm_ = copy.deepcopy(vm_) - if 'gateway' in svm_: - del svm_['gateway'] - if 'ssh_gateway' in vm_: - svm_['gateway'] = ssh_gateway_opts = {} - for k in ['ssh_gateway_key', - 'ssh_gateway', - 'ssh_gateway_user', - 'ssh_gateway_port']: - val = vm_.get(k, None) - if val: - ssh_gateway_opts[ssh_gateway_opts.get(k, k)] = val - sret = __salt__['saltify.create'](svm_) - changes['400_salt'] = 'This vm is now a salt minion' - if 'Error' in sret: - ret['result'] = False - changes['400_salt'] = pformat(sret['Error']) - else: - changed = True - ret['comment'] = changes['400_salt'] - _checkpoint(ret) - - changes['401_salt'] = 'Minion is alive for salt commands' - ping = salt.utils.cloud.wait_for_fun(testping, timeout=60) - if ping != 'OK': - ret['result'] = False - changes['401_salt'] = 'Unresponsive minion!' - ret['comment'] = changes['401_salt'] - _checkpoint(ret) - - sret = _checkpoint(ret) if not ret['result']: - ret['Error'] = 'Error while creating {0}'.format(vm_['name']) - if not changed and ret['result']: - ret['changes'] = {} - ret['comment'] = '\n{0}'.format(sret) + ret['Error'] = 'Error while creating {0},'.format(vm_['name']) return ret diff --git a/salt/daemons/masterapi.py b/salt/daemons/masterapi.py index 845bf9579f..c90c67098d 100644 --- a/salt/daemons/masterapi.py +++ b/salt/daemons/masterapi.py @@ -314,6 +314,41 @@ class RemoteFuncs(object): mopts['jinja_trim_blocks'] = self.opts['jinja_trim_blocks'] return mopts + def _ext_nodes(self, load): + ''' + Return the results from an external node classifier if one is + specified + ''' + if 'id' not in load: + log.error('Received call for external nodes without an id') + return {} + if not salt.utils.verify.valid_id(self.opts, load['id']): + return {} + # Evaluate all configured master_tops interfaces + + opts = {} + grains = {} + ret = {} + + if 'opts' in load: + opts = load['opts'] + if 'grains' in load['opts']: + grains = load['opts']['grains'] + for fun in self.tops: + if fun not in self.opts.get('master_tops', {}): + continue + try: + ret.update(self.tops[fun](opts=opts, grains=grains)) + except Exception as exc: + # If anything happens in the top generation, log it and move on + log.error( + 'Top function {0} failed with error {1} for minion ' + '{2}'.format( + fun, exc, load['id'] + ) + ) + return ret + def _mine_get(self, load): ''' Gathers the data from the specified minions' mine @@ -429,7 +464,14 @@ class RemoteFuncs(object): if os.path.isabs(load['path']) or '../' in load['path']: # Can overwrite master files!! return False + if not salt.utils.verify.valid_id(self.opts, load['id']): + return False file_recv_max_size = 1024*1024 * self.opts.get('file_recv_max_size', 100) + + if 'loc' in load and load['loc'] < 0: + log.error('Invalid file pointer: load[loc] < 0') + return False + if len(load['data']) + load.get('loc', 0) > file_recv_max_size: log.error( 'Exceeding file_recv_max_size limit: {0}'.format( @@ -560,7 +602,7 @@ class RemoteFuncs(object): return {} if not isinstance(self.opts['peer_run'], dict): return {} - if any(key not in load for key in ('fun', 'arg', 'id', 'tok')): + if any(key not in load for key in ('fun', 'arg', 'id')): return {} perms = set() for match in self.opts['peer_run']: @@ -573,6 +615,13 @@ class RemoteFuncs(object): if re.match(perm, load['fun']): good = True if not good: + # The minion is not who it says it is! + # We don't want to listen to it! + log.warn( + 'Minion id {0} is not who it says it is!'.format( + load['id'] + ) + ) return {} # Prepare the runner object opts = {'fun': load['fun'], @@ -589,7 +638,7 @@ class RemoteFuncs(object): Request the return data from a specific jid, only allowed if the requesting minion also initialted the execution. ''' - if any(key not in load for key in ('jid', 'id', 'tok')): + if any(key not in load for key in ('jid', 'id')): return {} # Check that this minion can access this data auth_cache = os.path.join( diff --git a/salt/master.py b/salt/master.py index 8c4da02fd2..ae58cb5ec7 100644 --- a/salt/master.py +++ b/salt/master.py @@ -19,13 +19,11 @@ try: except ImportError: # This is in case windows minion is importing pass import resource -import subprocess import multiprocessing import sys # Import third party libs import zmq -import yaml from M2Crypto import RSA # Import salt libs @@ -435,25 +433,6 @@ class Publisher(multiprocessing.Process): context.term() -class WorkerTrack(object): - def __init__(self, opts): - ''' - Watches the worker procs - ''' - self.opts = opts - self.counter = multiprocessing.Value('i', 0) - self.lock = multiprocessing.Lock() - - def finished(self): - ''' - To be called when a process finishes initializing - ''' - with self.lock: - self.counter.value += 1 - if int(self.opts['worker_threads']) == self.counter.value: - log.info('Master is ready to receive requests!') - - class ReqServer(object): ''' Starts up the master request server, minions send results to this @@ -476,7 +455,6 @@ class ReqServer(object): # Prepare the AES key self.key = key self.crypticle = crypticle - self.tracker = WorkerTrack(opts) def __bind(self): ''' @@ -496,14 +474,11 @@ class ReqServer(object): self.work_procs.append(MWorker(self.opts, self.master_key, self.key, - self.crypticle, - tracker=self.tracker) - ) + self.crypticle)) for ind, proc in enumerate(self.work_procs): log.info('Starting Salt worker process {0}'.format(ind)) proc.start() - log.info('Successfully started Salt worker process on PID: {0}'.format(proc.pid)) self.workers.bind(self.w_uri) @@ -592,8 +567,7 @@ class MWorker(multiprocessing.Process): opts, mkey, key, - crypticle, - tracker=None): + crypticle): multiprocessing.Process.__init__(self) self.opts = opts self.serial = salt.payload.Serial(opts) @@ -601,7 +575,6 @@ class MWorker(multiprocessing.Process): self.mkey = mkey self.key = key self.k_mtime = 0 - self.tracker = tracker def __bind(self): ''' @@ -615,7 +588,6 @@ class MWorker(multiprocessing.Process): log.info('Worker binding to socket {0}'.format(w_uri)) try: socket.connect(w_uri) - self.tracker.finished() while True: try: package = socket.recv() @@ -876,34 +848,6 @@ class AESFuncs(object): return {} load.pop('tok') ret = {} - # The old ext_nodes method is set to be deprecated in 0.10.4 - # and should be removed within 3-5 releases in favor of the - # "master_tops" system - if self.opts['external_nodes']: - if not salt.utils.which(self.opts['external_nodes']): - log.error(('Specified external nodes controller {0} is not' - ' available, please verify that it is installed' - '').format(self.opts['external_nodes'])) - return {} - cmd = '{0} {1}'.format(self.opts['external_nodes'], load['id']) - ndata = yaml.safe_load( - subprocess.Popen( - cmd, - shell=True, - stdout=subprocess.PIPE - ).communicate()[0]) - if 'environment' in ndata: - saltenv = ndata['environment'] - else: - saltenv = 'base' - - if 'classes' in ndata: - if isinstance(ndata['classes'], dict): - ret[saltenv] = list(ndata['classes']) - elif isinstance(ndata['classes'], list): - ret[saltenv] = ndata['classes'] - else: - return ret # Evaluate all configured master_tops interfaces opts = {} @@ -1152,7 +1096,7 @@ class AESFuncs(object): file_recv_max_size = 1024*1024 * self.opts.get('file_recv_max_size', 100) if 'loc' in load and load['loc'] < 0: - log.error('Should not happen: load[loc] < 0') + log.error('Invalid file pointer: load[loc] < 0') return False if len(load['data']) + load.get('loc', 0) > file_recv_max_size: @@ -1505,7 +1449,7 @@ class AESFuncs(object): load['timeout'] = int(clear_load['timeout']) except ValueError: msg = 'Failed to parse timeout value: {0}'.format( - clear_load['tmo']) + clear_load['timeout']) log.warn(msg) return {} if 'tgt_type' in clear_load: diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 5fdf9820b4..e15b292764 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -7,6 +7,7 @@ access to the master root execution access to all salt minions ''' # Import python libs +import time import functools import json import glob @@ -16,6 +17,7 @@ import shutil import subprocess import sys import traceback +from salt.utils import vt # Import salt libs import salt.utils @@ -242,7 +244,8 @@ def _run(cmd, timeout=None, with_communicate=True, reset_system_locale=True, - saltenv='base'): + saltenv='base', + use_vt=False): ''' Do the DRY thing and only call subprocess.Popen() once ''' @@ -423,38 +426,106 @@ def _run(cmd, .format(cwd) ) - # This is where the magic happens - try: - proc = salt.utils.timed_subprocess.TimedProc(cmd, **kwargs) - except (OSError, IOError) as exc: - raise CommandExecutionError( - 'Unable to run command {0!r} with the context {1!r}, reason: {2}' - .format(cmd, kwargs, exc) - ) + if not use_vt: + # This is where the magic happens + try: + proc = salt.utils.timed_subprocess.TimedProc(cmd, **kwargs) + except (OSError, IOError) as exc: + raise CommandExecutionError( + 'Unable to run command {0!r} with the context {1!r}, reason: {2}' + .format(cmd, kwargs, exc) + ) - try: - proc.wait(timeout) - except TimedProcTimeoutError as exc: - ret['stdout'] = str(exc) - ret['stderr'] = '' - ret['retcode'] = None + try: + proc.wait(timeout) + except TimedProcTimeoutError as exc: + ret['stdout'] = str(exc) + ret['stderr'] = '' + ret['retcode'] = None + ret['pid'] = proc.process.pid + # ok return code for timeouts? + ret['retcode'] = 1 + return ret + + out, err = proc.stdout, proc.stderr + + if rstrip: + if out is not None: + out = out.rstrip() + if err is not None: + err = err.rstrip() ret['pid'] = proc.process.pid - # ok return code for timeouts? - ret['retcode'] = 1 - return ret - - out, err = proc.stdout, proc.stderr - - if rstrip: - if out is not None: - out = out.rstrip() - if err is not None: - err = err.rstrip() - - ret['stdout'] = out - ret['stderr'] = err - ret['pid'] = proc.process.pid - ret['retcode'] = proc.process.returncode + ret['retcode'] = proc.process.returncode + ret['stdout'] = out + ret['stderr'] = err + else: + to = '' + if timeout: + to = ' (timeout: {0}s)'.format(timeout) + log.debug('Running {0} in VT{1}'.format(cmd, to)) + stdout, stderr = '', '' + now = time.time() + if timeout: + will_timeout = now + timeout + else: + will_timeout = -1 + try: + proc = vt.Terminal(cmd, + shell=True, + log_stdout=True, + log_stderr=True, + env=env, + log_stdin_level=output_loglevel, + log_stdout_level=output_loglevel, + log_stderr_level=output_loglevel, + stream_stdout=True, + stream_stderr=True) + # consume output + finished = False + ret['pid'] = proc.pid + while not finished: + try: + try: + time.sleep(0.5) + try: + cstdout, cstderr = proc.recv() + except IOError: + cstdout, cstderr = '', '' + if cstdout: + stdout += cstdout + else: + cstdout = '' + if cstderr: + stderr += cstderr + else: + cstderr = '' + if not cstdout and not cstderr and not proc.isalive(): + finished = True + if timeout and (time.time() > will_timeout): + ret['stderr'] = ( + 'SALT: Timeout after {0}s\n{1}').format( + timeout, stderr) + ret['retcode'] = None + break + except KeyboardInterrupt: + ret['stderr'] = 'SALT: User break\n{0}'.format(stderr) + ret['retcode'] = 1 + break + except vt.TerminalException as exc: + log.error( + 'VT: {0}'.format(exc), + exc_info=log.isEnabledFor(logging.DEBUG)) + ret = {'retcode': 1, 'pid': '2'} + break + # only set stdout on sucess as we already mangled in other + # cases + ret['stdout'] = stdout + if finished: + ret['stderr'] = stderr + ret['retcode'] = proc.exitstatus + ret['pid'] = proc.pid + finally: + proc.close(terminate=True, kill=True) try: __context__['retcode'] = ret['retcode'] except NameError: @@ -542,6 +613,7 @@ def run(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Execute the passed command and return the output as a string @@ -602,7 +674,8 @@ def run(cmd, quiet=quiet, timeout=timeout, reset_system_locale=reset_system_locale, - saltenv=saltenv) + saltenv=saltenv, + use_vt=use_vt) if 'pid' in ret and '__pub_jid' in kwargs: # Stuff the child pid in the JID file @@ -651,6 +724,7 @@ def run_stdout(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Execute a command, and only return the standard out @@ -695,7 +769,8 @@ def run_stdout(cmd, quiet=quiet, timeout=timeout, reset_system_locale=reset_system_locale, - saltenv=saltenv) + saltenv=saltenv, + use_vt=use_vt) lvl = _check_loglevel(output_loglevel, quiet) if lvl is not None: @@ -732,6 +807,7 @@ def run_stderr(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Execute a command and only return the standard error @@ -776,6 +852,7 @@ def run_stderr(cmd, quiet=quiet, timeout=timeout, reset_system_locale=reset_system_locale, + use_vt=use_vt, saltenv=saltenv) lvl = _check_loglevel(output_loglevel, quiet) @@ -813,6 +890,7 @@ def run_all(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Execute the passed command and return a dict of return data @@ -857,7 +935,8 @@ def run_all(cmd, quiet=quiet, timeout=timeout, reset_system_locale=reset_system_locale, - saltenv=saltenv) + saltenv=saltenv, + use_vt=use_vt) lvl = _check_loglevel(output_loglevel, quiet) if lvl is not None: @@ -893,6 +972,7 @@ def retcode(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Execute a shell command and return the command's return code. @@ -937,7 +1017,8 @@ def retcode(cmd, quiet=quiet, timeout=timeout, reset_system_locale=reset_system_locale, - saltenv=saltenv) + saltenv=saltenv, + use_vt=use_vt) lvl = _check_loglevel(output_loglevel, quiet) if lvl is not None: @@ -968,6 +1049,7 @@ def _retcode_quiet(cmd, reset_system_locale=True, ignore_retcode=False, saltenv='base', + use_vt=False, **kwargs): ''' Helper for running commands quietly for minion startup. @@ -988,6 +1070,7 @@ def _retcode_quiet(cmd, reset_system_locale=reset_system_locale, ignore_retcode=ignore_retcode, saltenv=saltenv, + use_vt=use_vt, **kwargs) @@ -1007,6 +1090,7 @@ def script(source, reset_system_locale=True, __env__=None, saltenv='base', + use_vt=False, **kwargs): ''' Download a script from a remote location and execute the script locally. @@ -1091,7 +1175,8 @@ def script(source, umask=umask, timeout=timeout, reset_system_locale=reset_system_locale, - saltenv=saltenv) + saltenv=saltenv, + use_vt=use_vt) _cleanup_tempfile(path) return ret @@ -1109,6 +1194,8 @@ def script_retcode(source, reset_system_locale=True, __env__=None, saltenv='base', + output_loglevel='debug', + use_vt=False, **kwargs): ''' Download a script from a remote location and execute the script locally. @@ -1157,6 +1244,8 @@ def script_retcode(source, timeout=timeout, reset_system_locale=reset_system_locale, saltenv=saltenv, + output_loglevel=output_loglevel, + use_vt=use_vt, **kwargs)['retcode'] diff --git a/salt/modules/config.py b/salt/modules/config.py index 67afe485f7..4bb3fcb868 100644 --- a/salt/modules/config.py +++ b/salt/modules/config.py @@ -265,7 +265,7 @@ def dot_vals(value): return ret -def gather_bootstrap_script(): +def gather_bootstrap_script(bootstrap=None): ''' Download the salt-bootstrap script, and return the first location downloaded to. @@ -276,6 +276,6 @@ def gather_bootstrap_script(): salt '*' config.gather_bootstrap_script ''' - ret = salt.utils.cloud.update_bootstrap(__opts__) + ret = salt.utils.cloud.update_bootstrap(__opts__, url=bootstrap) if 'Success' in ret and len(ret['Success']['Files updated']) > 0: return ret['Success']['Files updated'][0] diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index 4924f5092e..b8bd1bcc91 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -13,16 +13,21 @@ from __future__ import print_function import traceback import datetime import pipes +import copy import logging import tempfile import os +import time import shutil import re import random # Import salt libs import salt +from salt.utils.odict import OrderedDict import salt.utils +from salt.utils.dictupdate import update as dictupdate +from salt.utils import vt import salt.utils.cloud import salt.config import salt._compat @@ -37,6 +42,8 @@ __func_alias__ = { DEFAULT_NIC_PROFILE = {'eth0': {'link': 'br0', 'type': 'veth'}} +SEED_MARKER = '/lxc.initial_seed' +_marker = object() def _ip_sort(ip): @@ -51,6 +58,187 @@ def _ip_sort(ip): return '{0}___{1}'.format(idx, ip) +def cloud_init_interface(name, vm_=None, **kwargs): + ''' + Interface between salt.cloud.lxc driver and lxc.init + vm_ is a mapping of vm opts in the salt.cloud format + as documented for the lxc driver. + This can be used either: + - from the salt cloud driver + - because you find the argument to give easier here + than using directly lxc.init + WARNING: BE REALLY CAREFUL CHANGING DEFAULTS !!! + IT'S A RETRO COMPATIBLE INTERFACE WITH + THE SALT CLOUD DRIVER (ask kiorky). + + name + name of the lxc container to create + from_container + which container we use as a template + when running lxc.clone + image + which template do we use when we + are using lxc.create. This is the default + mode unless you specify something in from_container + backing + which backing store to use. + Values can be: overlayfs, dir(default), lvm, zfs, brtfs + fstype + When using a blockdevice level backing store, + which filesystem to use on + size + When using a blockdevice level backing store, + which size for the filesystem to use on + snapshot + Use snapshot when cloning the container source + vgname + if using LVM: vgname + lgname + if using LVM: lvname + pub_key + public key to preseed the minion with. + Can be the keycontent or a filepath + priv_key + private key to preseed the minion with. + Can be the keycontent or a filepath + ip + ip for the primary nic + mac + mac for the primary nic + netmask + netmask for the primary nic (24) + = vm_.get('netmask', '24') + bridge + bridge^for the primary nic (lxcbr0) + gateway + network gateway for the container + unconditionnal_install + given to lxc.bootstrap (see relative doc) + force_install + given to lxc.bootstrap (see relative doc) + config + any extra argument for the salt minion config + dnsservers + dns servers to set inside the container + autostart + autostart the container at boot time + password + administrative password for the container + users + administrative users for the contrainer + default: [root] and [root, ubuntu] on ubuntu + ''' + if vm_ is None: + vm_ = {} + vm_ = copy.deepcopy(vm_) + vm_ = dictupdate(vm_, kwargs) + profile = _lxc_profile(vm_.get('profile', {})) + if name is None: + name = vm_['name'] + from_container = vm_.get('from_container', None) + # if we are on ubuntu, default to ubuntu + default_template = '' + if __grains__.get('os', '') in ['Ubuntu']: + default_template = 'ubuntu' + image = vm_.get('image', profile.get('template', + default_template)) + vgname = vm_.get('vgname', None) + backing = vm_.get('backing', 'dir') + snapshot = vm_.get('snapshot', False) + autostart = bool(vm_.get('autostart', True)) + dnsservers = vm_.get('dnsservers', []) + if not dnsservers: + dnsservers = ['8.8.8.8', '4.4.4.4'] + password = vm_.get('password', 's3cr3t') + fstype = vm_.get('fstype', None) + lvname = vm_.get('lvname', None) + pub_key = vm_.get('pub_key', None) + priv_key = vm_.get('priv_key', None) + size = vm_.get('size', '20G') + script = vm_.get('script', None) + script_args = vm_.get('script_args', None) + if image: + profile['template'] = image + if vgname: + profile['vgname'] = vgname + if backing: + profile['backing'] = backing + users = vm_.get('users', None) + if users is None: + users = [] + ssh_username = vm_.get('ssh_username', None) + if ssh_username and (ssh_username not in users): + users.append(ssh_username) + ip = vm_.get('ip', None) + mac = vm_.get('mac', None) + netmask = vm_.get('netmask', '24') + bridge = vm_.get('bridge', 'lxcbr0') + gateway = vm_.get('gateway', 'auto') + unconditionnal_install = vm_.get('unconditionnal_install', False) + force_install = vm_.get('force_install', True) + config = vm_.get('config', {}) + if not config: + config = vm_.get('minion', {}) + if not config: + config = {} + config.setdefault('master', + vm_.get('master', + __opts__.get('master', + __opts__['id']))) + config.setdefault( + 'master_port', + vm_.get('master_port', + __opts__.get('master_port', + __opts__.get('ret_port', + __opts__.get('4506'))))) + if not config['master']: + config = {} + eth0 = {} + nic_opts = {'eth0': eth0} + bridge = vm_.get('bridge', 'lxcbr0') + if ip is None: + nic_opts = None + else: + fullip = ip + if netmask: + fullip += '/{0}'.format(netmask) + eth0['ipv4'] = fullip + if mac is not None: + eth0['hwaddr'] = mac + if bridge: + eth0['link'] = bridge + gateway = vm_.get('gateway', 'auto') + # + lxc_init_interface = {} + lxc_init_interface['name'] = name + lxc_init_interface['config'] = config + lxc_init_interface['memory'] = 0 # nolimit + lxc_init_interface['pub_key'] = pub_key + lxc_init_interface['priv_key'] = priv_key + lxc_init_interface['bridge'] = bridge + lxc_init_interface['gateway'] = gateway + lxc_init_interface['nic_opts'] = nic_opts + lxc_init_interface['clone'] = from_container + lxc_init_interface['profile'] = profile + lxc_init_interface['snapshot'] = snapshot + lxc_init_interface['dnsservers'] = dnsservers + lxc_init_interface['fstype'] = fstype + lxc_init_interface['vgname'] = vgname + lxc_init_interface['size'] = size + lxc_init_interface['lvname'] = lvname + lxc_init_interface['force_install'] = force_install + lxc_init_interface['unconditionnal_install'] = ( + unconditionnal_install + ) + lxc_init_interface['bootstrap_url'] = script + lxc_init_interface['bootstrap_args'] = script_args + lxc_init_interface['bootstrap_shell'] = '/bin/bash' + lxc_init_interface['autostart'] = autostart + lxc_init_interface['users'] = users + lxc_init_interface['password'] = password + return lxc_init_interface + + def __virtual__(): if salt.utils.which('lxc-start'): return True @@ -82,6 +270,16 @@ def _lxc_profile(profile): Profiles can be defined in the config or pillar, e.g.: + Profile can be a string to be retrieven in config + or a mapping. + + If is is a mapping and it contains a name, the name will + be used to grab defaults in config as if the script was called + with a string. This let you override either all opts or just specific ones. + + The resulting profile will be cached inside the context for further + quick access + .. code-block:: yaml lxc.profile: @@ -90,8 +288,31 @@ def _lxc_profile(profile): backing: lvm vgname: lxc size: 1G + + :: + + salt-call lxc.profile ubuntu + salt-call lxc.profile \\ + {'name': 'ubuntu', 'template': 'myapp', \\ + 'backing': 'overlayfs'} + ''' - return __salt__['config.option']('lxc.profile', {}).get(profile, {}) + profilename = profile + if isinstance(profile, dict): + profilename = profile.get('name', 'ubuntu') + else: + profile = {} + key = 'lxc.profile.{0}'.format(profilename) + rprofile = __context__.get(key, {}) + if not rprofile: + default_profile = __salt__['config.option']( + 'lxc.profile', {}).get(profilename, {}) + # save the resulting profile in the context + rprofile = dictupdate( + copy.deepcopy(default_profile), + copy.deepcopy(profile)) + __context__[key] = rprofile + return rprofile def _rand_cpu_str(cpu): @@ -110,61 +331,155 @@ def _rand_cpu_str(cpu): return ','.join(sorted(to_set)) -def _config_list(**kwargs): - ''' - Return a list of dicts from the salt level configurations - ''' +def _get_network_conf(conf_tuples=None, **kwargs): + nic = kwargs.pop('nic', None) ret = [] - memory = kwargs.pop('memory', None) - if memory: - memory = memory * 1024 * 1024 - ret.append({'lxc.cgroup.memory.limit_in_bytes': memory}) - cpuset = kwargs.pop('cpuset', None) - if cpuset: - ret.append({'lxc.cgroup.cpuset.cpus': cpuset}) - cpushare = kwargs.pop('cpushare', None) - if cpushare: - ret.append({'lxc.cgroup.cpu.shares': cpushare}) - cpu = kwargs.pop('cpu') - if cpu and not cpuset: - ret.append({'lxc.cgroup.cpuset.cpus': _rand_cpu_str(cpu)}) - nic = kwargs.pop('nic') + if not nic: + return ret + kwargs = copy.deepcopy(kwargs) + gateway = kwargs.pop('gateway', None) + bridge = kwargs.get('bridge', None) + if not conf_tuples: + conf_tuples = [] if nic: nicp = __salt__['config.option']('lxc.nic', {}).get( - nic, DEFAULT_NIC_PROFILE - ) + nic, DEFAULT_NIC_PROFILE + ) nic_opts = kwargs.pop('nic_opts', None) - for dev, args in nicp.items(): ret.append({'lxc.network.type': args.pop('type', 'veth')}) ret.append({'lxc.network.name': dev}) ret.append({'lxc.network.flags': args.pop('flags', 'up')}) - opts = nic_opts.get(dev) if nic_opts else None + opts = nic_opts.get(dev) if nic_opts else {} + mac = opts.get('mac', '') if opts: - mac = opts.get('mac') ipv4 = opts.get('ipv4') ipv6 = opts.get('ipv6') else: ipv4, ipv6 = None, None - mac = salt.utils.gen_mac() - ret.append({'lxc.network.hwaddr': mac}) + if not mac: + mac = salt.utils.gen_mac() + if mac: + ret.append({'lxc.network.hwaddr': mac}) if ipv4: ret.append({'lxc.network.ipv4': ipv4}) if ipv6: ret.append({'lxc.network.ipv6': ipv6}) for k, v in args.items(): + if k == 'link' and bridge: + v = bridge + v = opts.get(k, v) ret.append({'lxc.network.{0}'.format(k): v}) + # gateway (in automode) must be appended following network conf ! + if gateway is not None: + ret.append({'lxc.network.ipv4.gateway': gateway}) + + old = _get_veths(conf_tuples) + new = _get_veths(ret) + # verify that we did not loose the mac settings + for iface in [a for a in new]: + if iface in old: + ndata = new[iface] + odata = old[iface] + omac = odata.get('lxc.network.hwaddr', '') + nmac = ndata.get('lxc.network.hwaddr', '') + if omac and not nmac: + new[iface]['lxc.network.hwaddr'] = omac + ret = [] + for v in new.values(): + for row in v: + ret.append({row: v[row]}) return ret +def _get_memory(memory): + ''' + Handle the saltcloud driver and lxc runner memory restriction + differences. + Runner limits to 1024MB by default + SaltCloud does not restrict memory usage by default + ''' + if memory is None: + memory = 1024 + if memory: + memory = memory * 1024 * 1024 + return memory + + +def _get_autostart(autostart): + if autostart is None: + autostart = True + if autostart: + autostart = '1' + else: + autostart = '0' + return autostart + + +def _get_lxc_default_data(**kwargs): + kwargs = copy.deepcopy(kwargs) + ret = {} + autostart = _get_autostart(kwargs.pop('autostart', None)) + ret['lxc.start.auto'] = autostart + memory = _get_memory(kwargs.pop('memory', None)) + if memory: + ret['lxc.cgroup.memory.limit_in_bytes'] = memory + cpuset = kwargs.pop('cpuset', None) + if cpuset: + ret['lxc.cgroup.cpuset.cpus'] = cpuset + cpushare = kwargs.pop('cpushare', None) + cpu = kwargs.pop('cpu', None) + if cpushare: + ret['lxc.cgroup.cpu.shares'] = cpushare + if cpu and not cpuset: + ret['lxc.cgroup.cpuset.cpus'] = _rand_cpu_str(cpu) + return ret + + +def _config_list(conf_tuples=None, **kwargs): + ''' + Return a list of dicts from the salt level configurations + ''' + if not conf_tuples: + conf_tuples = [] + kwargs = copy.deepcopy(kwargs) + ret = [] + default_data = _get_lxc_default_data(**kwargs) + for k, val in default_data.items(): + ret.append({k: val}) + net_datas = _get_network_conf(conf_tuples=conf_tuples, **kwargs) + ret.extend(net_datas) + return ret + + +def _get_veths(net_data): + '''Parse the nic setup inside lxc conf tuples back + to a dictionnary indexed by network interface''' + if isinstance(net_data, dict): + net_data = net_data.items() + nics = OrderedDict() + current_nic = OrderedDict() + for item in net_data: + if item and isinstance(item, dict): + item = item.items()[0] + if item[0] == 'lxc.network.type': + current_nic = OrderedDict() + if item[0] == 'lxc.network.name': + nics[item[1].strip()] = current_nic + current_nic[item[0].strip()] = item[1].strip() + return nics + + class _LXCConfig(object): ''' LXC configuration data ''' pattern = re.compile(r'^(\S+)(\s*)(=)(\s*)(.*)') + non_interpretable_pattern = re.compile(r'^((#.*)|(\s*))$') def __init__(self, **kwargs): + kwargs = copy.deepcopy(kwargs) self.name = kwargs.pop('name', None) self.data = [] if self.name: @@ -175,6 +490,10 @@ class _LXCConfig(object): match = self.pattern.findall((l.strip())) if match: self.data.append((match[0][0], match[0][-1])) + match = self.non_interpretable_pattern.findall( + (l.strip())) + if match: + self.data.append(('', match[0][0])) else: self.path = None @@ -183,52 +502,39 @@ class _LXCConfig(object): self._filter_data(k) self.data.append((k, v)) - memory = kwargs.pop('memory', None) - if memory: - memory = memory * 1024 * 1024 - _replace('lxc.cgroup.memory.limit_in_bytes', memory) - cpuset = kwargs.pop('cpuset', None) - _replace('lxc.cgroup.cpuset.cpus', cpuset) - cpushare = kwargs.pop('cpushare', None) - _replace('lxc.cgroup.cpu.shares', cpushare) + default_data = _get_lxc_default_data(**kwargs) + for k, val in default_data.items(): + _replace(k, val) + old_net = self._filter_data('lxc.network') + net_datas = _get_network_conf(conf_tuples=old_net, **kwargs) + if net_datas: + for row in net_datas: + self.data.extend(row.items()) - nic = kwargs.pop('nic') - if nic: - self._filter_data('lxc.network') - nicp = __salt__['config.option']('lxc.nic', {}).get( - nic, DEFAULT_NIC_PROFILE - ) - nic_opts = kwargs.pop('nic_opts', None) - - for dev, args in nicp.items(): - self.data.append(('lxc.network.type', - args.pop('type', 'veth'))) - self.data.append(('lxc.network.name', dev)) - self.data.append(('lxc.network.flags', - args.pop('flags', 'up'))) - opts = nic_opts.get(dev) if nic_opts else None - if opts: - mac = opts.get('mac') - ipv4 = opts.get('ipv4') - ipv6 = opts.get('ipv6') - else: - ipv4, ipv6 = None, None - mac = salt.utils.gen_mac() - self.data.append(('lxc.network.hwaddr', mac)) - if ipv4: - self.data.append(('lxc.network.ipv4', ipv4)) - if ipv6: - self.data.append(('lxc.network.ipv6', ipv6)) - for k, v in args.items(): - self.data.append(('lxc.network.{0}'.format(k), v)) + # be sure to reset harmful settings + for i in ['lxc.cgroup.memory.limit_in_bytes']: + if not default_data.get(i): + self._filter_data(i) def as_string(self): - return '\n'.join( - ['{0} = {1}'.format(k, v) for k, v in self.data]) + '\n' + chunks = [] + + def _process(item): + sep = ' = ' + if not item[0]: + sep = '' + chunks.append('{0[0]}{1}{0[1]}'.format(item, sep)) + map(_process, self.data) + return '\n'.join(chunks) + '\n' def write(self): if self.path: - salt.utils.fopen(self.path, 'w').write(self.as_string()) + content = self.as_string() + # 2 step rendering to be sure not to open/wipe the config + # before as_string suceeds. + with open(self.path, 'w') as fic: + fic.write(content) + fic.flush() def tempfile(self): # this might look like the function name is shadowing the @@ -239,11 +545,15 @@ class _LXCConfig(object): return f def _filter_data(self, pat): + removed = [] x = [] for i in self.data: if not re.match('^' + pat, i[0]): x.append(i) + else: + removed.append(i) self.data = x + return removed def get_base(**kwargs): @@ -262,7 +572,7 @@ def get_base(**kwargs): [seed=(True|False)] [install=(True|False)] \\ [config=minion_config] ''' - cntrs = ls() + cntrs = __salt__['lxc.ls']() if kwargs.get('image'): image = kwargs.get('image') proto = salt._compat.urlparse(image).scheme @@ -273,19 +583,19 @@ def get_base(**kwargs): __salt__['config.option']('hash_type')) name = '__base_{0}_{1}_{2}'.format(proto, img_name, hash_) if name not in cntrs: - create(name, **kwargs) + __salt__['lxc.create'](name, **kwargs) if kwargs.get('vgname'): rootfs = os.path.join('/dev', kwargs['vgname'], name) - lxc_info = info(name) + lxc_info = __salt__['lxc.info'](name) edit_conf(lxc_info['config'], **{'lxc.rootfs': rootfs}) return name elif kwargs.get('template'): name = '__base_{0}'.format(kwargs['template']) if name not in cntrs: - create(name, **kwargs) + __salt__['lxc.create'](name, **kwargs) if kwargs.get('vgname'): rootfs = os.path.join('/dev', kwargs['vgname'], name) - lxc_info = info(name) + lxc_info = __salt__['lxc.info'](name) edit_conf(lxc_info['config'], **{'lxc.rootfs': rootfs}) return name return '' @@ -294,15 +604,33 @@ def get_base(**kwargs): def init(name, cpuset=None, cpushare=None, - memory=1024, + memory=None, nic='default', profile=None, nic_opts=None, cpu=None, + autostart=True, + password=None, + users=None, + dnsservers=None, + bridge=None, + gateway=None, + pub_key=None, + priv_key=None, + force_install=False, + unconditionnal_install=False, + bootstrap_args=None, + bootstrap_shell=None, + bootstrap_url=None, **kwargs): ''' Initialize a new container. + This is a partial indempotent function as if it is already + provisionned, we will reset a bit the lxc configuration + file but much of the hard work will be escaped as + markers will prevent re-execution of harmfull tasks. + CLI Example: .. code-block:: bash @@ -313,8 +641,11 @@ def init(name, [nic_opts=nic_opts] [start=(True|False)] \\ [seed=(True|False)] [install=(True|False)] \\ [config=minion_config] [approve_key=(True|False) \\ - [clone=original] - + [clone=original] [autostart=True] \\ + [priv_key=/path_or_content] [pub_key=/path_or_content] \\ + [bridge=lxcbr0] [gateway=10.0.3.1] \\ + [dnsservers[dns1,dns2]] \\ + [users=[foo]] password='secret' name Name of the container. @@ -328,22 +659,51 @@ def init(name, cpushare cgroups cpu shares. + autostart + autostart container on reboot + memory cgroups memory limit, in MB. + (0 for nolimit, None for old default 1024MB) + + gateway + the ipv4 gateway to use + the default does nothing more than lxcutils does + + bridge + the bridge to use + the default does nothing more than lxcutils does nic Network interfaces profile (defined in config or pillar). + users + Sysadmins users to set the administrative password to + eg [root, ubuntu, sysadmin], default [root] and [root, ubuntu] + on ubuntu + + password + Set the initial password for default sysadmin users, at least root + but also can be used for sudoers, eg [root, ubuntu, sysadmin] + profile A LXC profile (defined in config or pillar). + This can be either a real profile mapping or a string + to retrieve it in configuration nic_opts Extra options for network interfaces. E.g: - {"eth0": {"mac": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1", "ipv6": "2001:db8::ff00:42:8329"}} - + {"eth0": {"mac": "aa:bb:cc:dd:ee:ff", + "ipv4": "10.1.1.1", "ipv6": "2001:db8::ff00:42:8329"}} + or + {"eth0": {"mac": "aa:bb:cc:dd:ee:ff", + "ipv4": "10.1.1.1/24", "ipv6": "2001:db8::ff00:42:8329"}} start Start the newly created container. + dnsservers + list of dns servers to set in the container, default [] (no setting) + seed Seed the container with the minion config. Default: ``True`` @@ -351,26 +711,74 @@ def init(name, If salt-minion is not already installed, install it. Default: ``True`` config - Optional config parameters. By default, the id is set to the name of the - container. + Optional config parameters. By default, the id is set to + the name of the container. + + pub_key + Explicit public key to preseed the minion with (optionnal). + This can be either a filepath or a string representing the key + + priv_key + Explicit private key to preseed the minion with (optionnal). + This can be either a filepath or a string representing the key approve_key + If explicit preseeding is not used; Attempt to request key approval from the master. Default: ``True`` clone Original from which to use a clone operation to create the container. Default: ``None`` + + bootstrap_url + See lxc.bootstrap + * + bootstrap_shell + See lxc.bootstrap + + bootstrap_args + See lxc.bootstrap + + force_install + Force installation even if salt-minion is detected, + this is the way to run vendor bootstrap scripts even + if a salt minion is already present in the container + + unconditionnal_install + Run the script even if the container seems seeded ''' - profile = _lxc_profile(profile) + kwargs = copy.deepcopy(kwargs) + comment = '' + ret = {'error': '', 'name': name, 'result': True} + changes = ret.setdefault('changes', {}) + if users is None: + users = [] + dusers = ['root'] + if ( + __grains__['os'] in ['Ubuntu'] + and 'ubuntu' not in users + ): + dusers.append('ubuntu') + for user in dusers: + if user not in users: + users.append(user) + if not isinstance(profile, dict): + profile = _lxc_profile(profile) + profile = copy.deepcopy(profile) def select(k, default=None): - kw = kwargs.pop(k, None) + kw = kwargs.pop(k, _marker) p = profile.pop(k, default) - return kw or p + # let kwargs be really be the preferred choice + if kw is _marker: + kw = p + return kw tvg = select('vgname') vgname = tvg if tvg else __salt__['config.option']('lxc.vgname') start_ = select('start', True) + ret['started'] = start_ + autostart = select('autostart', autostart) seed = select('seed', True) install = select('install', True) seed_cmd = select('seed_cmd') @@ -383,43 +791,216 @@ def init(name, clone_from = get_base(vgname=vgname, **kwargs) if not kwargs.get('snapshot') is False: kwargs['snapshot'] = True - - if clone_from: - ret = __salt__['lxc.clone'](name, clone_from, - profile=profile, **kwargs) + exists = __salt__['lxc.exists'](name) + to_reboot = False + remove_seed_marker = False + if exists: + comment += 'Container already exists\n' + elif clone_from: + remove_seed_marker = True + ret.update( + __salt__['lxc.clone'](name, clone_from, + profile=profile, **kwargs)) if not ret.get('cloned', False): return ret cfg = _LXCConfig(name=name, nic=nic, nic_opts=nic_opts, - cpuset=cpuset, cpushare=cpushare, memory=memory) + bridge=bridge, gateway=gateway, + autostart=autostart, + cpuset=cpuset, cpushare=cpushare, memory=memory) + old_chunks = __salt__['lxc.read_conf'](cfg.path) cfg.write() + chunks = __salt__['lxc.read_conf'](cfg.path) + if old_chunks != chunks: + to_reboot = True else: + remove_seed_marker = True cfg = _LXCConfig(nic=nic, nic_opts=nic_opts, cpuset=cpuset, - cpushare=cpushare, memory=memory) + bridge=bridge, gateway=gateway, + autostart=autostart, + cpushare=cpushare, memory=memory) with cfg.tempfile() as cfile: - ret = __salt__['lxc.create'](name, config=cfile.name, - profile=profile, **kwargs) + ret.update( + __salt__['lxc.create'](name, config=cfile.name, + profile=profile, **kwargs)) if not ret.get('created', False): return ret path = '/var/lib/lxc/{0}/config'.format(name) - for comp in _config_list(cpu=cpu, nic=nic, nic_opts=nic_opts, cpuset=cpuset, cpushare=cpushare, memory=memory): + old_chunks = [] + if os.path.exists(path): + old_chunks = __salt__['lxc.read_conf'](path) + for comp in _config_list(conf_tuples=old_chunks, + cpu=cpu, + nic=nic, nic_opts=nic_opts, bridge=bridge, + cpuset=cpuset, cpushare=cpushare, + memory=memory): edit_conf(path, **comp) - ret['state'] = start(name)['state'] - ret['name'] = name - if seed: - ret['seeded'] = __salt__['lxc.bootstrap']( - name, config=salt_config, approve_key=approve_key, install=install) - elif seed_cmd: - lxc_info = info(name) - rootfs = lxc_info['rootfs'] - ret['seeded'] = __salt__[seed_cmd](rootfs, name, salt_config) + chunks = __salt__['lxc.read_conf'](path) + if old_chunks != chunks: + to_reboot = True + if remove_seed_marker: + lxcret = __salt__['lxc.run_cmd']( + name, 'rm -f \"{0}\"'.format(SEED_MARKER), + stdout=False, stderr=False) + + # last time to be sure any of our property is correctly applied + cfg = _LXCConfig(name=name, nic=nic, nic_opts=nic_opts, + bridge=bridge, gateway=gateway, + autostart=autostart, + cpuset=cpuset, cpushare=cpushare, memory=memory) + old_chunks = [] + if os.path.exists(cfg.path): + old_chunks = __salt__['lxc.read_conf'](cfg.path) + cfg.write() + chunks = __salt__['lxc.read_conf'](cfg.path) + if old_chunks != chunks: + comment += 'Container configuration updated\n' + to_reboot = True + else: + if not to_reboot: + comment += 'Container already correct\n' + if to_reboot: + __salt__['lxc.stop'](name) + if clone_from: + inner = 'cloned' + comment += 'Container cloned\n' + else: + inner = 'created' + comment += 'Container created\n' + ret[inner] = True + if ( + not exists + or ( + exists + and __salt__['lxc.state'](name) != 'running' + ) + ): + ret['state'] = __salt__['lxc.start'](name) + ret['state'] = __salt__['lxc.state'](name) + + # set the default user/password, only the first time + if password: + changes['250_password'] = 'Passwords in place\n' + gid = '/.lxc.initial_pass'.format(name) + gids = [gid, + '/lxc.initial_pass', + '/.lxc.{0}.initial_pass'.format(name)] + lxcrets = [] + for ogid in gids: + lxcrets.append( + bool(__salt__['lxc.run_cmd']( + name, 'test -e {0}'.format(gid), + stdout=False, stderr=False))) + if True not in lxcrets: + cret = __salt__['lxc.set_pass'](name, + password=password, users=users) + changes['250_password'] = 'Password updated\n' + if not cret['result']: + ret['result'] = False + changes['250_password'] = 'Failed to update passwords\n' + try: + lxcret = int( + __salt__['lxc.run_cmd']( + name, + 'sh -c \'touch "{0}"; ' + 'test -e "{0}";echo ${{?}}\''.format(gid))) + except ValueError: + lxcret = 1 + ret['result'] = not bool(lxcret) + if not cret['result']: + changes['250_password'] = 'Failed to test password file marker' + comment += changes['250_password'] + if not ret['result']: + ret['comment'] = comment + return ret + + # set dns servers if any, only the first time + if dnsservers: + changes['350_dns'] = 'DNS in place\n' + # retro compatibility, test also old markers + gid = '/.lxc.initial_dns' + gids = [gid, + '/lxc.initial_dns', + '/lxc.{0}.initial_dns'.format(name)] + lxcrets = [] + for ogid in gids: + lxcrets.append(bool( + __salt__['lxc.run_cmd']( + name, 'test -e {0}'.format(ogid), + stdout=False, stderr=False))) + if True not in lxcrets: + cret = __salt__['lxc.set_dns'](name, dnsservers=dnsservers) + changes['350_dns'] = 'DNS updated\n' + if not cret['result']: + ret['result'] = False + changes['350_dns'] = 'DNS provisionning error\n' + try: + lxcret = int( + __salt__['lxc.run_cmd']( + name, + 'sh -c \'touch "{0}"; ' + 'test -e "{0}";echo ${{?}}\''.format(gid))) + except ValueError: + lxcret = 1 + ret['result'] = not lxcret + if not cret['result']: + changes['350_dns'] = 'Failed to set DNS marker\n' + comment += changes['350_dns'] + if not ret['result']: + ret['comment'] = comment + return ret + + if seed or seed_cmd: + changes['450_seed'] = 'Container seeded\n' + if seed: + ret['seeded'] = __salt__['lxc.bootstrap']( + name, config=salt_config, + approve_key=approve_key, + pub_key=pub_key, priv_key=priv_key, + install=install, + force_install=force_install, + unconditionnal_install=unconditionnal_install, + bootstrap_url=bootstrap_url, + bootstrap_shell=bootstrap_shell, + bootstrap_args=bootstrap_args) + elif seed_cmd: + lxc_info = info(name) + rootfs = lxc_info['rootfs'] + ret['seeded'] = __salt__[seed_cmd](rootfs, name, salt_config) + if not ret['seeded']: + ret['result'] = False + changes['450_seed'] = 'Seeding error\n' + comment += changes['450_seed'] + if not ret['seeded']: + ret['comment'] = comment + ret['result'] = False + return ret + else: + ret['seeded'] = True + if not start_: stop(name) ret['state'] = 'stopped' + comment += 'Container stopped\n' else: ret['state'] = state(name) + ret['comment'] = comment + ret['mid'] = name return ret +def cloud_init(name, vm_=None, **kwargs): + '''Thin wrapper to lxc.init to be used from the saltcloud lxc driver + name + Name of the container + may be None and then guessed from saltcloud mapping + vm_ + saltcloud mapping defaults for the vm + ''' + init_interface = __salt__['lxc.cloud_init_interface'](name, vm_, **kwargs) + name = init_interface.pop('name', name) + return __salt__['lxc.init'](name, **init_interface) + + def create(name, config=None, profile=None, options=None, **kwargs): ''' Create a new container. @@ -464,6 +1045,7 @@ def create(name, config=None, profile=None, options=None, **kwargs): options Template specific options to pass to the lxc-create command. ''' + kwargs = copy.deepcopy(kwargs) if exists(name): return {'created': False, 'error': 'container already exists'} @@ -471,11 +1053,15 @@ def create(name, config=None, profile=None, options=None, **kwargs): if not isinstance(profile, dict): profile = _lxc_profile(profile) + profile = copy.deepcopy(profile) def select(k, default=None): - kw = kwargs.pop(k, None) + kw = kwargs.pop(k, _marker) p = profile.pop(k, default) - return kw or p + # let kwargs be really be the preferred choice + if kw is _marker: + kw = p + return kw tvg = select('vgname') vgname = tvg if tvg else __salt__['config.option']('lxc.vgname') @@ -487,6 +1073,12 @@ def create(name, config=None, profile=None, options=None, **kwargs): fstype = select('fstype') size = select('size', '1G') image = select('image') + if backing in ['dir', 'overlayfs']: + fstype = None + size = None + # some backends wont support some parameters + if backing in ['aufs', 'dir', 'overlayfs']: + lvname = vgname = None if image: img_tar = __salt__['cp.cache_file'](image) @@ -572,6 +1164,10 @@ def clone(name, salt '*' lxc.clone myclone ubuntu "snapshot=True" ''' + if not isinstance(profile, dict): + profile = _lxc_profile(profile) + kwargs = copy.deepcopy(kwargs) + profile = copy.deepcopy(profile) if exists(name): return {'cloned': False, 'error': 'container already exists'} @@ -584,23 +1180,26 @@ def clone(name, return {'cloned': False, 'error': 'original container \'{0}\' is running'.format(orig)} + def select(k, default=None): + kw = kwargs.pop(k, _marker) + p = profile.pop(k, default) + # let kwargs be really be the preferred choice + if kw is _marker: + kw = p + return kw + + backing = select('backing') + if backing in ['dir']: + snapshot = False if not snapshot: snapshot = '' else: snapshot = '-s' + cmd = 'lxc-clone {2} -o {0} -n {1}'.format(orig, name, snapshot) - - if not isinstance(profile, dict): - profile = _lxc_profile(profile) - - def select(k, default=None): - kw = kwargs.pop(k, None) - p = profile.pop(k, default) - return kw or p - - backing = select('backing') size = select('size', '1G') - + if backing in ['dir', 'overlayfs']: + size = None if size: cmd += ' -L {0}'.format(size) if backing: @@ -615,8 +1214,10 @@ def clone(name, cmd = 'lxc-destroy -n {0}'.format(name) __salt__['cmd.retcode'](cmd) log.warn('lxc-clone failed to create container') - return {'cloned': False, 'error': - 'container could not be created with cmd "{0}": {1}'.format(cmd, ret['stderr'])} + return {'cloned': False, 'error': ( + 'container could not be created' + ' with cmd "{0}": {1}' + ).format(cmd, ret['stderr'])} def ls(): @@ -1018,10 +1619,16 @@ def info(name): ret['config'] = f if ret['state'] == 'running': - limit = int(get_parameter(name, 'memory.limit_in_bytes').get( - 'memory.limit_in_bytes')) - usage = int(get_parameter(name, 'memory.usage_in_bytes').get( - 'memory.usage_in_bytes')) + try: + limit = int(get_parameter(name, 'memory.limit_in_bytes').get( + 'memory.limit_in_bytes')) + except (TypeError, ValueError): + limit = 0 + try: + usage = int(get_parameter(name, 'memory.usage_in_bytes').get( + 'memory.usage_in_bytes')) + except (TypeError, ValueError): + usage = 0 free = limit - usage ret['memory_limit'] = limit ret['memory_free'] = free @@ -1244,7 +1851,14 @@ def set_dns(name, dnsservers=None, searchdomains=None): return ret -def bootstrap(name, config=None, approve_key=True, install=True): +def bootstrap(name, config=None, approve_key=True, + install=True, + pub_key=None, priv_key=None, + bootstrap_url=None, + force_install=False, + unconditionnal_install=False, + bootstrap_args=None, + bootstrap_shell=None): ''' Install and configure salt in a container. @@ -1258,13 +1872,39 @@ def bootstrap(name, config=None, approve_key=True, install=True): to the target host's master. approve_key - Request a pre-approval of the generated minion key. Requires + Request a pre-approval of the generated minion key. Requires that the salt-master be configured to either auto-accept all keys or expect a signing request from the target host. Default: ``True`` + + pub_key + Explicit public key to pressed the minion with (optionnal). + This can be either a filepath or a string representing the key + + priv_key + Explicit private key to pressed the minion with (optionnal). + This can be either a filepath or a string representing the key + + bootstrap_url + url, content or filepath to the salt bootstrap script + + bootstrap_args + salt bootstrap script arguments + + bootstrap_shell + shell to execute the script into + install Whether to attempt a full installation of salt-minion if needed. + force_install + Force installation even if salt-minion is detected, + this is the way to run vendor bootstrap scripts even + if a salt minion is already present in the container + + unconditionnal_install + Run the script even if the container seems seeded + CLI Example: .. code-block:: bash @@ -1275,6 +1915,12 @@ def bootstrap(name, config=None, approve_key=True, install=True): infos = __salt__['lxc.info'](name) if not infos: return None + # default setted here as we cant set them + # in def as it can come from a chain of procedures. + if not bootstrap_args: + bootstrap_args = '-c {0}' + if not bootstrap_shell: + bootstrap_shell = 'sh' prior_state = _ensure_running(name) if not prior_state: @@ -1283,47 +1929,90 @@ def bootstrap(name, config=None, approve_key=True, install=True): cmd = 'bash -c "if type salt-minion; then ' \ 'salt-call --local service.stop salt-minion; exit 0; ' \ 'else exit 1; fi"' - needs_install = bool(__salt__['lxc.run_cmd'](name, cmd, stdout=False)) - - tmp = tempfile.mkdtemp() - cfg_files = __salt__['seed.mkconfig'](config, tmp=tmp, id_=name, - approve_key=approve_key) - - if needs_install: - if install: - rstr = __salt__['test.rand_str']() - configdir = '/tmp/.c_{0}'.format(rstr) - run_cmd(name, 'install -m 0700 -d {0}'.format(configdir)) - bs_ = __salt__['config.gather_bootstrap_script']() - cp(name, bs_, '/tmp/bootstrap.sh') - cp(name, cfg_files['config'], os.path.join(configdir, 'minion')) - cp(name, cfg_files['privkey'], os.path.join(configdir, 'minion.pem')) - cp(name, cfg_files['pubkey'], os.path.join(configdir, 'minon.pub')) - - cmd = 'PATH=$PATH:/bin:/sbin:/usr/sbin sh /tmp/bootstrap.sh -c {0}'.format(configdir) - res = not __salt__['lxc.run_cmd'](name, cmd, stdout=False) - else: - res = False + if not force_install: + # no need to run this cmd in force mode + needs_install = bool(__salt__['lxc.run_cmd'](name, cmd, stdout=False)) else: - minion_config = salt.config.minion_config(cfg_files['config']) - pki_dir = minion_config['pki_dir'] - cp(name, cfg_files['config'], '/etc/salt/minion') - cp(name, cfg_files['privkey'], os.path.join(pki_dir, 'minion.pem')) - cp(name, cfg_files['pubkey'], os.path.join(pki_dir, 'minion.pub')) - run_cmd(name, 'salt-call --local service.enable salt-minion', - stdout=False) + needs_install = True + seeded = not __salt__['lxc.run_cmd']( + name, 'test -e \"{0}\"'.format(SEED_MARKER), stdout=False, stderr=False) + tmp = tempfile.mkdtemp() + if seeded and not unconditionnal_install: res = True - - shutil.rmtree(tmp) - if prior_state == 'stopped': - __salt__['lxc.stop'](name) - elif prior_state == 'frozen': - __salt__['lxc.freeze'](name) + else: + res = False + cfg_files = __salt__['seed.mkconfig']( + config, tmp=tmp, id_=name, approve_key=approve_key, + priv_key=priv_key, pub_key=pub_key) + if needs_install or force_install or unconditionnal_install: + if install: + rstr = __salt__['test.rand_str']() + configdir = '/tmp/.c_{0}'.format(rstr) + run_cmd(name, 'install -m 0700 -d {0}'.format(configdir)) + bs_ = __salt__['config.gather_bootstrap_script']( + bootstrap=bootstrap_url) + cp(name, bs_, '/tmp/bootstrap.sh') + cp(name, cfg_files['config'], + os.path.join(configdir, 'minion')) + cp(name, cfg_files['privkey'], + os.path.join(configdir, 'minion.pem')) + cp(name, cfg_files['pubkey'], + os.path.join(configdir, 'minion.pub')) + bootstrap_args = bootstrap_args.format(configdir) + cmd = ('PATH=$PATH:/bin:/sbin:/usr/sbin' + ' {0} /tmp/bootstrap.sh {1}').format( + bootstrap_shell, bootstrap_args) + # log ASAP the forged bootstrap command which can be wrapped + # out of the output in case of unexpected problem + log.info('Running {0} in lxc {1}'.format(cmd, name)) + res = not __salt__['lxc.run_cmd']( + name, cmd, + stdout=True, stderr=True, use_vt=True)['retcode'] + else: + res = False + else: + minion_config = salt.config.minion_config(cfg_files['config']) + pki_dir = minion_config['pki_dir'] + cp(name, cfg_files['config'], '/etc/salt/minion') + cp(name, cfg_files['privkey'], os.path.join(pki_dir, 'minion.pem')) + cp(name, cfg_files['pubkey'], os.path.join(pki_dir, 'minion.pub')) + run_cmd(name, 'salt-call --local service.enable salt-minion', + stdout=False) + res = True + shutil.rmtree(tmp) + if prior_state == 'stopped': + __salt__['lxc.stop'](name) + elif prior_state == 'frozen': + __salt__['lxc.freeze'](name) + # mark seeded upon sucessful install + if res: + __salt__['lxc.run_cmd']( + name, 'sh -c \'touch "{0}";\''.format(SEED_MARKER)) return res +def attachable(name): + ''' + Return True if the named container can be attached to via the lxc-attach + command + + CLI Example: + + .. code-block:: bash + + salt 'minion' lxc.attachable ubuntu + ''' + cmd = 'lxc-attach -n {0} -- /usr/bin/env'.format(name) + data = __salt__['cmd.run_all'](cmd) + if not data['retcode']: + return True + if data['stderr'].startswith('lxc-attach: failed to get the init pid'): + return False + return False + + def run_cmd(name, cmd, no_start=False, preserve_state=True, - stdout=True, stderr=False): + stdout=True, stderr=False, use_vt=False): ''' Run a command inside the container. @@ -1354,6 +2043,9 @@ def run_cmd(name, cmd, no_start=False, preserve_state=True, stderr: Return stderr. Default: ``False`` + use_vt + use saltstack utils.vt to stream output to console + .. note:: If stderr and stdout are both ``False``, the return code is returned. @@ -1364,8 +2056,60 @@ def run_cmd(name, cmd, no_start=False, preserve_state=True, if not prior_state: return prior_state if attachable(name): - res = __salt__['cmd.run_all']( - 'lxc-attach -n \'{0}\' -- env -i {1}'.format(name, cmd)) + if not use_vt: + cmd = 'lxc-attach -n \'{0}\' -- env -i {1}'.format(name, cmd) + res = __salt__['cmd.run_all'](cmd) + else: + stdout, stderr = '', '' + cmd = 'lxc-attach -n \'{0}\' -- env -i {1}'.format(name, cmd) + try: + proc = vt.Terminal(cmd, + shell=True, + log_stdin_level='info', + log_stdout_level='info', + log_stderr_level='info', + log_stdout=True, + log_stderr=True, + stream_stdout=True, + stream_stderr=True) + # consume output + while 1: + try: + time.sleep(0.5) + try: + cstdout, cstderr = proc.recv() + except IOError: + cstdout, cstderr = '', '' + if cstdout: + stdout += cstdout + else: + cstdout = '' + if cstderr: + stderr += cstderr + else: + cstderr = '' + # done by vt itself + # if stdout: + # log.debug(stdout) + # if stderr: + # log.debug(stderr) + if not cstdout and not cstderr and not proc.isalive(): + break + except KeyboardInterrupt: + break + res = {'retcode': proc.exitstatus, + 'pid': 2, + 'stdout': stdout, + 'stderr': stderr} + except vt.TerminalException: + trace = traceback.format_exc() + log.error(trace) + res = {'retcode': 127, + 'pid': '2', + 'stdout': stdout, + 'stderr': stderr} + finally: + proc.terminate() else: rootfs = info(name).get('rootfs') res = __salt__['cmd.run_chroot'](rootfs, cmd) @@ -1411,33 +2155,36 @@ def cp(name, src, dest): if not dest_name: dest_name = src_name - cmd = 'cat {0} | lxc-attach -n {1} -- env -i tee {2} > /dev/null'.format( - src, name, os.path.join(dest_dir, dest_name)) - log.info(cmd) - ret = __salt__['cmd.run_all'](cmd) + # before touching to existing file which may disturb any running + # process, check that the md5sum are different + cmd = 'md5sum {0} 2> /dev/null'.format(src) + csrcmd5 = __salt__['cmd.run_all'](cmd) + srcmd5 = csrcmd5['stdout'].split()[0] + + cmd = 'lxc-attach -n {0} -- env -i md5sum {1} 2> /dev/null'.format( + name, dest) + cdestmd5 = __salt__['cmd.run_all'](cmd) + if not cdestmd5['retcode']: + try: + destmd5 = cdestmd5['stdout'].split()[0] + except(TypeError, IndexError, IndexError): + destmd5 = '' + else: + destmd5 = '' + ret = { + 'pid': 2, + 'retcode': '0', + 'stdout': '', + 'stderr': '', + } + if srcmd5 != destmd5: + cmd = 'cat {0} | lxc-attach -n {1} -- env -i tee {2} > /dev/null'.format( + src, name, dest) + log.info(cmd) + ret = __salt__['cmd.run_all'](cmd) return ret -def attachable(name): - ''' - Return True if the named container can be attached to via the lxc-attach - command - - CLI Example: - - .. code-block:: bash - - salt 'minion' lxc.attachable ubuntu - ''' - cmd = 'lxc-attach -n {0} -- /usr/bin/env'.format(name) - data = __salt__['cmd.run_all'](cmd) - if not data['retcode']: - return True - if data['stderr'].startswith('lxc-attach: failed to get the init pid'): - return False - return False - - def read_conf(conf_file, out_format='simple'): ''' Read in an LXC configuration file. By default returns a simple, unsorted @@ -1462,8 +2209,8 @@ def read_conf(conf_file, out_format='simple'): comps = line.split('=') value = '='.join(comps[1:]).strip() comment = None - if '#' in value: - vcomps = value.split('#') + if value.strip().startswith('#'): + vcomps = value.strip().split('#') value = vcomps[1].strip() comment = '#'.join(vcomps[1:]).strip() ret_commented.append({comps[0].strip(): { @@ -1556,7 +2303,7 @@ def edit_conf(conf_file, out_format='simple', **kwargs): data = [] try: - conf = read_conf(conf_file, out_format='commented') + conf = __salt__['lxc.read_conf'](conf_file, out_format='commented') except Exception: conf = [] @@ -1569,9 +2316,7 @@ def edit_conf(conf_file, out_format='simple', **kwargs): if key not in kwargs: data.append(line) continue - data.append({ - key: kwargs[key] - }) + data.append({key: kwargs[key]}) del kwargs[key] for kwarg in kwargs: @@ -1579,5 +2324,5 @@ def edit_conf(conf_file, out_format='simple', **kwargs): continue data.append({kwarg: kwargs[kwarg]}) - write_conf(conf_file, data) + __salt__['lxc.write_conf'](conf_file, data) return read_conf(conf_file, out_format) diff --git a/salt/modules/postgres.py b/salt/modules/postgres.py index 55cb4ae59c..8bc1000a54 100644 --- a/salt/modules/postgres.py +++ b/salt/modules/postgres.py @@ -238,7 +238,11 @@ def psql_query(query, user=None, host=None, port=None, maintenance_db=None, password=None, runas=None): ''' Run an SQL-Query and return the results as a list. This command - only supports SELECT statements. + only supports SELECT statements. This limitation can be worked around + with a query like this: + + WITH updated AS (UPDATE pg_authid SET rolconnlimit = 2000 WHERE + rolname = 'rolename' RETURNING rolconnlimit) SELECT * FROM updated; CLI Example: diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 849453a0ad..28e43ff249 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -500,6 +500,8 @@ def clear_cache(): ''' Forcibly removes all caches on a minion. + .. versionadded:: Helium + WARNING: The safest way to clear a minion cache is by first stopping the minion and then deleting the cache files before restarting it. diff --git a/salt/modules/seed.py b/salt/modules/seed.py index 0481f2cbc8..c7e5b15989 100644 --- a/salt/modules/seed.py +++ b/salt/modules/seed.py @@ -25,6 +25,13 @@ __func_alias__ = { } +def _file_or_content(file_): + if os.path.exists(file_): + with open(file_) as fic: + return fic.read() + return file_ + + def _mount(path, ftype): mpt = None if ftype == 'block': @@ -141,10 +148,17 @@ def _prep_bootstrap(mpt): shutil.copy(bs_, os.path.join(mpt, 'tmp')) -def mkconfig(config=None, tmp=None, id_=None, approve_key=True): +def mkconfig(config=None, tmp=None, id_=None, approve_key=True, + pub_key=None, priv_key=None): ''' Generate keys and config and put them in a tmp directory. + pub_key + absolute path or file content of an optionnal preseeded salt key + + priv_key + absolute path or file content of an optionnal preseeded salt key + CLI Example: .. code-block:: bash @@ -167,11 +181,19 @@ def mkconfig(config=None, tmp=None, id_=None, approve_key=True): fp_.write(yaml.dump(config, default_flow_style=False)) # Generate keys for the minion - salt.crypt.gen_keys(tmp, 'minion', 2048) pubkeyfn = os.path.join(tmp, 'minion.pub') privkeyfn = os.path.join(tmp, 'minion.pem') - - if approve_key: + preseeded = pub_key and priv_key + if preseeded: + with open(pubkeyfn, 'w') as fic: + fic.write(_file_or_content(pub_key)) + with open(privkeyfn, 'w') as fic: + fic.write(_file_or_content(priv_key)) + os.chmod(pubkeyfn, 0600) + os.chmod(privkeyfn, 0600) + else: + salt.crypt.gen_keys(tmp, 'minion', 2048) + if approve_key and not preseeded: with salt.utils.fopen(pubkeyfn) as fp_: pubkey = fp_.read() __salt__['pillar.ext']({'virtkey': [id_, pubkey]}) diff --git a/salt/returners/couchbase_return.py b/salt/returners/couchbase_return.py index 5a1978fa0b..48b89e7437 100644 --- a/salt/returners/couchbase_return.py +++ b/salt/returners/couchbase_return.py @@ -121,7 +121,7 @@ def _get_ttl(): ''' Return the TTL that we should store our objects with ''' - return __opts__['keep_jobs'] * 60 * 60, # keep_jobs is in hours + return __opts__['keep_jobs'] * 60 * 60 # keep_jobs is in hours #TODO: add to returner docs-- this is a new one diff --git a/salt/runners/lxc.py b/salt/runners/lxc.py index 211c4b36be..a237a33cd2 100644 --- a/salt/runners/lxc.py +++ b/salt/runners/lxc.py @@ -7,13 +7,22 @@ Control Linux Containers via Salt # Import python libs from __future__ import print_function +import time +import os +import copy +import logging # Import Salt libs +from salt.utils.odict import OrderedDict import salt.client +import salt.output import salt.utils.virt +import salt.utils.cloud import salt.key +log = logging.getLogger(__name__) + # Don't shadow built-in's. __func_alias__ = { 'list_': 'list' @@ -106,12 +115,11 @@ def find_guests(names): return ret -def init(names, - host=None, - **kwargs): +def init(names, host=None, saltcloud_mode=False, quiet=False, **kwargs): ''' Initialize a new container + .. code-block:: bash salt-run lxc.init name host=minion_id [cpuset=cgroups_cpuset] \\ @@ -129,6 +137,10 @@ def init(names, host Minion to start the container on. Required. + saltcloud_mode + init the container with the saltcloud opts format instead + See lxc.init_interface module documentation + cpuset cgroups cpuset. @@ -152,83 +164,190 @@ def init(names, nic_opts Extra options for network interfaces. E.g: - {"eth0": {"mac": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1", "ipv6": "2001:db8::ff00:42:8329"}} + {"eth0": {"mac": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1", + "ipv6": "2001:db8::ff00:42:8329"}} start Start the newly created container. seed - Seed the container with the minion config and autosign its key. Default: true + Seed the container with the minion config and autosign its key. + Default: true install If salt-minion is not already installed, install it. Default: true config - Optional config parameters. By default, the id is set to the name of the - container. + Optional config parameters. By default, the id is set to + the name of the container. ''' + ret = {'comment': '', 'result': True} if host is None: - #TODO: Support selection of host based on available memory/cpu/etc. - print('A host must be provided') - return False - names = names.split(',') - print('Searching for LXC Hosts') + # TODO: Support selection of host based on available memory/cpu/etc. + ret['comment'] = 'A host must be provided' + ret['result'] = False + return ret + if isinstance(names, basestring): + names = names.split(',') + if not isinstance(names, list): + ret['comment'] = 'Container names are not formed as a list' + ret['result'] = False + return ret + log.info('Searching for LXC Hosts') data = __salt__['lxc.list'](host, quiet=True) for host, containers in data.items(): for name in names: if name in sum(containers.values(), []): - print('Container \'{0}\' already exists on host \'{1}\''.format( - name, host)) - return False - + log.info('Container \'{0}\' already exists' + ' on host \'{1}\',' + ' init can be a NO-OP'.format( + name, host)) if host not in data: - print('Host \'{0}\' was not found'.format(host)) - return False - - kw = dict((k, v) for k, v in kwargs.items() if not k.startswith('__')) - approve_key = kw.get('approve_key', True) - if approve_key: - for name in names: - kv = salt.utils.virt.VirtKey(host, name, __opts__) - if kv.authorize(): - print('Container key will be preauthorized') - else: - print('Container key preauthorization failed') - return False + ret['comment'] = 'Host \'{0}\' was not found'.format(host) + ret['result'] = False + return ret client = salt.client.get_local_client(__opts__['conf_file']) - print('Creating container(s) \'{0}\' on host \'{1}\''.format(names, host)) + kw = dict((k, v) for k, v in kwargs.items() if not k.startswith('__')) + pub_key = kw.get('pub_key', None) + priv_key = kw.get('priv_key', None) + explicit_auth = pub_key and priv_key + approve_key = kw.get('approve_key', True) + seeds = {} + if approve_key and not explicit_auth: + for name in names: + seeds[name] = kwargs.get('seed', True) + try: + ping = client.cmd(name, 'test.ping', timeout=20).get(name, None) + except (TypeError, KeyError): + ping = False + curkey = os.path.join(__opts__['pki_dir'], 'minions', name) + # be sure not to seed an alrady seeded host + if ping or os.path.exists(curkey): + seeds[name] = False + kv = salt.utils.virt.VirtKey(host, name, __opts__) + if kv.authorize(): + log.info('Container key will be preauthorized') + else: + ret['comment'] = 'Container key preauthorization failed' + ret['result'] = False + return ret + + log.info('Creating container(s) \'{0}\'' + ' on host \'{1}\''.format(names, host)) cmds = [] - ret = {} for name in names: args = [name] - cmds.append(client.cmd_iter(host, - 'lxc.init', - args, - kwarg=kwargs, - timeout=600)) - ret = {} - for cmd in cmds: - sub_ret = next(cmd) - if sub_ret and host in sub_ret: - if host in ret: - ret[host].append(sub_ret[host]['ret']) - else: - ret[host] = [sub_ret[host]['ret']] - else: - ret = {} + kw = kwargs + if saltcloud_mode: + kw = copy.deepcopy(kw) + kw['name'] = name + kw = client.cmd( + host, 'lxc.cloud_init_interface', args + [kw], + expr_form='list', timeout=600).get(host, {}) + name = kw.pop('name', name) + # be sure not to seed an alrady seeded host + kw['seed'] = seeds[name] + if not kw['seed']: + kw.pop('seed_cmd', '') + cmds.append( + (host, + name, + client.cmd_iter(host, 'lxc.init', args, kwarg=kw, timeout=600))) + done = ret.setdefault('done', []) + errors = ret.setdefault('errors', OrderedDict()) - for host, returns in ret.items(): - for j_ret in returns: - if j_ret.get('created', False) or j_ret.get('cloned', False): - print('Container \'{0}\' initialized on host \'{1}\''.format( - j_ret.get('name'), host)) + for ix, acmd in enumerate(cmds): + hst, container_name, cmd = acmd + containers = ret.setdefault(hst, []) + herrs = errors.setdefault(hst, OrderedDict()) + serrs = herrs.setdefault(container_name, []) + sub_ret = next(cmd) + error = None + if isinstance(sub_ret, dict) and host in sub_ret: + j_ret = sub_ret[hst] + container = j_ret.get('ret', {}) + if container and isinstance(container, dict): + if not container.get('result', False): + error = container else: - error = j_ret.get('error', 'unknown error') - print('Container \'{0}\' was not initialized: {1}'.format(j_ret.get(name), error)) - return ret or None + error = 'Invalid return for {0}'.format(container_name) + else: + error = sub_ret + if not error: + error = 'unknown error (no return)' + if error: + ret['result'] = False + serrs.append(error) + else: + container['container_name'] = name + containers.append(container) + done.append(container) + + # marking ping status as True only and only if we have at + # least provisionned one container + ret['ping_status'] = bool(len(done)) + + # for all provisionned containers, last job is to verify + # - the key status + # - we can reach them + for container in done: + # explicitly check and update + # the minion key/pair stored on the master + container_name = container['container_name'] + key = os.path.join(__opts__['pki_dir'], 'minions', container_name) + if explicit_auth: + fcontent = '' + if os.path.exists(key): + with open(key) as fic: + fcontent = fic.read().strip() + if pub_key.strip() != fcontent: + with open(key, 'w') as fic: + fic.write(pub_key) + fic.flush() + mid = j_ret.get('mid', None) + if not mid: + continue + + def testping(**kw): + mid_ = kw['mid'] + ping = client.cmd(mid_, 'test.ping', timeout=20) + time.sleep(1) + if ping: + return 'OK' + raise Exception('Unresponsive {0}'.format(mid_)) + ping = salt.utils.cloud.wait_for_fun(testping, timeout=21, mid=mid) + if ping != 'OK': + ret['ping_status'] = False + ret['result'] = False + + # if no lxc detected as touched (either inited or verified + # we result to False + if not done: + ret['result'] = False + if not quiet: + salt.output.display_output(ret, '', __opts__) + return ret + + +def cloud_init(names, host=None, quiet=False, **kwargs): + ''' + Wrapper for using lxc.init in saltcloud compatibility mode + + names + Name of the containers, supports a single name or a comma delimited + list of names. + + host + Minion to start the container on. Required. + + saltcloud_mode + init the container with the saltcloud opts format instead + ''' + return __salt__['lxc.init'](names=names, host=host, + saltcloud_mode=True, quiet=quiet, **kwargs) def _list_iter(host=None): diff --git a/salt/states/cmd.py b/salt/states/cmd.py index 47460d47c3..e1ec147912 100644 --- a/salt/states/cmd.py +++ b/salt/states/cmd.py @@ -352,6 +352,8 @@ def wait(name, env=(), stateful=False, umask=None, + output_loglevel='debug', + use_vt=False, **kwargs): ''' Run the given command only if the watch statement calls it @@ -412,6 +414,16 @@ def wait(name, Only run if the file specified by ``creates`` does not exist. .. versionadded:: Helium + + output_loglevel + Control the loglevel at which the output from the command is logged. + Note that the command being run will still be logged (loglevel: DEBUG) + regardless, unless ``quiet`` is used for this value. + + use_vt + Use VT utils (saltstack) to stream the command output more + interactively to the console and the logs. + This is experimental. ''' # Ignoring our arguments is intentional. return {'name': name, @@ -436,6 +448,8 @@ def wait_script(name, env=None, stateful=False, umask=None, + use_vt=False, + output_loglevel='debug', **kwargs): ''' Download a script from a remote source and execute it only if a watch @@ -503,6 +517,17 @@ def wait_script(name, stateful The command being executed is expected to return data about executing a state + + use_vt + Use VT utils (saltstack) to stream the command output more + interactively to the console and the logs. + This is experimental. + + output_loglevel + Control the loglevel at which the output from the command is logged. + Note that the command being run will still be logged (loglevel: DEBUG) + regardless, unless ``quiet`` is used for this value. + ''' # Ignoring our arguments is intentional. return {'name': name, @@ -522,9 +547,10 @@ def run(name, env=None, stateful=False, umask=None, - output_loglevel='info', + output_loglevel='debug', quiet=False, timeout=None, + use_vt=False, **kwargs): ''' Run a command if certain circumstances are met. Use ``cmd.wait`` if you @@ -584,7 +610,7 @@ def run(name, output_loglevel Control the loglevel at which the output from the command is logged. - Note that the command being run will still be logged at loglevel INFO + Note that the command being run will still be logged (loglevel: DEBUG) regardless, unless ``quiet`` is used for this value. quiet @@ -602,6 +628,11 @@ def run(name, .. versionadded:: Helium + use_vt + Use VT utils (saltstack) to stream the command output more + interactively to the console and the logs. + This is experimental. + .. note:: cmd.run supports the usage of ``reload_modules``. This functionality @@ -651,6 +682,7 @@ def run(name, cmd_kwargs = {'cwd': cwd, 'runas': user, + 'use_vt': use_vt, 'shell': shell or __grains__['shell'], 'env': env, 'umask': umask, @@ -700,6 +732,8 @@ def script(name, stateful=False, umask=None, timeout=None, + use_vt=False, + output_loglevel='debug', **kwargs): ''' Download a script and execute it with specified arguments. @@ -780,6 +814,17 @@ def script(name, Only run if the file specified by ``creates`` does not exist. .. versionadded:: Helium + + use_vt + Use VT utils (saltstack) to stream the command output more + interactively to the console and the logs. + This is experimental. + + output_loglevel + Control the loglevel at which the output from the command is logged. + Note that the command being run will still be logged (loglevel: DEBUG) + regardless, unless ``quiet`` is used for this value. + ''' ret = {'name': name, 'changes': {}, @@ -815,6 +860,8 @@ def script(name, 'template': template, 'umask': umask, 'timeout': timeout, + 'output_loglevel': output_loglevel, + 'use_vt': use_vt, 'saltenv': __env__}) run_check_cmd_kwargs = { @@ -876,6 +923,8 @@ def call(name, onlyif=None, unless=None, creates=None, + output_loglevel='debug', + use_vt=False, **kwargs): ''' Invoke a pre-defined Python function with arguments specified in the state @@ -918,6 +967,8 @@ def call(name, 'runas': kwargs.get('user'), 'shell': kwargs.get('shell') or __grains__['shell'], 'env': kwargs.get('env'), + 'use_vt': use_vt, + 'output_loglevel': output_loglevel, 'umask': kwargs.get('umask')} if HAS_GRP: pgid = os.getegid() @@ -952,6 +1003,8 @@ def wait_call(name, unless=None, creates=None, stateful=False, + use_vt=False, + output_loglevel='debug', **kwargs): # Ignoring our arguments is intentional. return {'name': name, diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index becc6a355a..48d009231e 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -8,6 +8,7 @@ import os import sys import codecs import shutil +import hashlib import socket import tempfile import time @@ -1948,28 +1949,61 @@ def delete_minion_cachedir(minion_id, provider, opts, base=None): os.remove(path) -def update_bootstrap(config): +def update_bootstrap(config, url=None): + ''' Update the salt-bootstrap script + + url can be either: + + - The URL to fetch the bootstrap script from + - The absolute path to the bootstrap + - The content of the bootstrap script + + ''' - log.debug('Updating the bootstrap-salt.sh script to latest stable') - try: - import requests - except ImportError: - return {'error': ( - 'Updating the bootstrap-salt.sh script requires the ' - 'Python requests library to be installed' - )} - url = 'https://bootstrap.saltstack.com' - req = requests.get(url) - if req.status_code != 200: - return {'error': ( - 'Failed to download the latest stable version of the ' - 'bootstrap-salt.sh script from {0}. HTTP error: ' - '{1}'.format( - url, req.status_code - ) - )} + default_url = config.get('bootstrap_script__url', + 'https://bootstrap.saltstack.com') + if not url: + url = default_url + if not url: + raise ValueError('Cant get any source to update') + if (url.startswith('http')) or ('://' in url): + log.debug('Updating the bootstrap-salt.sh script to latest stable') + try: + import requests + except ImportError: + return {'error': ( + 'Updating the bootstrap-salt.sh script requires the ' + 'Python requests library to be installed' + )} + req = requests.get(url) + if req.status_code != 200: + return {'error': ( + 'Failed to download the latest stable version of the ' + 'bootstrap-salt.sh script from {0}. HTTP error: ' + '{1}'.format( + url, req.status_code + ) + )} + script_content = req.text + if url == default_url: + script_name = 'bootstrap-salt.sh' + else: + script_name = os.path.basename(url) + elif os.path.exists(url): + with open(url) as fic: + script_content = fic.read() + script_name = os.path.basename(url) + # in last case, assuming we got a script content + else: + script_content = url + script_name = '{0}.sh'.format( + hashlib.sha1(script_content).hexdigest() + ) + + if not script_content: + raise ValueError('No content in bootstrap script !') # Get the path to the built-in deploy scripts directory builtin_deploy_dir = os.path.join( @@ -2044,11 +2078,11 @@ def update_bootstrap(config): ) continue - deploy_path = os.path.join(entry, 'bootstrap-salt.sh') + deploy_path = os.path.join(entry, script_name) try: finished_full.append(deploy_path) with salt.utils.fopen(deploy_path, 'w') as fp_: - fp_.write(req.text) + fp_.write(script_content) except (OSError, IOError) as err: log.debug( 'Failed to write the updated script: {0}'.format(err)