From e297fd41026f5265921a0872157f2f8568b5bceb Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Sat, 8 Nov 2014 23:28:12 -0600 Subject: [PATCH] Complete LXC refactor --- salt/modules/lxc.py | 362 +++++++++++++++++++++++++++++--------------- 1 file changed, 244 insertions(+), 118 deletions(-) diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index b00608d8e8..1d0f807acc 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -47,12 +47,7 @@ __func_alias__ = { 'ls_': 'ls' } -# init does a lot of stuff. Use highstate outputter for a prettier summary -__outputter__ = { - 'init': 'highstate' -} - -DEFAULT_NIC_PROFILE = {'eth0': {'link': 'br0', 'type': 'veth'}} +DEFAULT_NIC_PROFILE = {'eth0': {'link': 'br0', 'type': 'veth', 'flags': 'up'}} SEED_MARKER = '/lxc.initial_seed' PATH = 'PATH=/bin:/usr/bin:/sbin:/usr/sbin:/opt/bin:' \ '/usr/local/bin:/usr/local/sbin' @@ -155,7 +150,7 @@ def cloud_init_interface(name, vm_=None, **kwargs): ip ip for the primary nic mac - mac for the primary nic + mac address for the primary nic netmask netmask for the primary nic (24) = ``vm_.get('netmask', '24')`` @@ -167,20 +162,17 @@ def cloud_init_interface(name, vm_=None, **kwargs): additional ips which will be wired on the main bridge (br0) which is connected to internet. Be aware that you may use manual virtual mac addresses - provided by you provider (online, ovh, etc). - This is a list of mappings ``{ip: '', mac: '',netmask:''}`` - Set gateway to ``None`` and an interface with a gateway - to escape from another interface that's eth0. - e.g.: - - .. code-block:: python - - {'mac': '00:16:3e:01:29:40', - 'gateway': None, #default - 'link': 'br0', #default - 'netmask': '', #default - 'ip': '22.1.4.25'} + providen by you provider (online, ovh, etc). + This is a list of mappings {ip: '', mac: '', netmask:''} + Set gateway to None and an interface with a gateway + to escape from another interface that eth0. + eg:: + - {'mac': '00:16:3e:01:29:40', + 'gateway': None, (default) + 'link': 'br0', (default) + 'netmask': '', (default) + 'ip': '22.1.4.25'} unconditional_install given to lxc.bootstrap (see relative doc) force_install @@ -280,7 +272,7 @@ def cloud_init_interface(name, vm_=None, **kwargs): fullip += '/{0}'.format(netmask) eth0['ipv4'] = fullip if mac is not None: - eth0['hwaddr'] = mac + eth0['mac'] = mac if bridge: eth0['link'] = bridge for ix, iopts in enumerate(vm_.get("additional_ips", [])): @@ -295,7 +287,7 @@ def cloud_init_interface(name, vm_=None, **kwargs): nm = iopts.get('netmask', '') if nm: ethx['ipv4'] += '/{0}'.format(nm) - for i in ['mac', 'hwaddr']: + for i in ('mac', 'hwaddr'): if i in iopts: ethx['hwaddr'] = iopts[i] if 'hwaddr' not in ethx: @@ -405,7 +397,7 @@ def get_network_profile(name=None): .. code-block:: python - {'eth0': {'link': 'br0', 'type': 'veth'}} + {'eth0': {'link': 'br0', 'type': 'veth', 'flags': 'up'}} Profiles can be defined in the minion or master config files, or in pillar or grains, and are loaded using :mod:`config.get @@ -474,7 +466,10 @@ def _network_conf(conf_tuples=None, **kwargs): if nic_opts: for dev, args in nic_opts.items(): ethx = nicp.setdefault(dev, {}) - ethx = salt.utils.dictupdate.update(ethx, args) + try: + ethx = salt.utils.dictupdate.update(ethx, args) + except AttributeError: + raise SaltInvocationError('Invalid nic_opts configuration') ifs = [a for a in nicp] ifs.sort() gateway_set = False @@ -484,7 +479,7 @@ def _network_conf(conf_tuples=None, **kwargs): ret.append({'lxc.network.name': dev}) ret.append({'lxc.network.flags': args.pop('flags', 'up')}) opts = nic_opts.get(dev) if nic_opts else {} - hwaddr = opts.get('hwaddr', '') + mac = opts.get('mac', '') if opts: ipv4 = opts.get('ipv4') ipv6 = opts.get('ipv6') @@ -492,8 +487,8 @@ def _network_conf(conf_tuples=None, **kwargs): ipv4, ipv6 = None, None if not mac: mac = salt.utils.gen_mac() - if hwaddr: - ret.append({'lxc.network.hwaddr': hwaddr}) + if mac: + ret.append({'lxc.network.hwaddr': mac}) if ipv4: ret.append({'lxc.network.ipv4': ipv4}) if ipv6: @@ -502,7 +497,7 @@ def _network_conf(conf_tuples=None, **kwargs): if key == 'link' and bridge: val = bridge val = opts.get(key, val) - if key in ('gateway', 'hwaddr'): + if key in ('gateway', 'mac'): continue ret.append({'lxc.network.{0}'.format(key): val}) # gateway (in automode) must be appended following network conf ! @@ -711,25 +706,28 @@ class _LXCConfig(object): return removed -def get_base(**kwargs): +def _get_base(**kwargs): ''' If the needed base does not exist, then create it, if it does exist create nothing and return the name of the base lxc container so it can be cloned. - - CLI Example: - - .. code-block:: bash - - salt 'minion' lxc.init name [cpuset=cgroups_cpuset] \\ - [nic=nic_profile] [profile=lxc_profile] \\ - [nic_opts=nic_opts] [image=network image path]\\ - [seed=(True|False)] [install=(True|False)] \\ - [config=minion_config] ''' - cntrs = ls_() - if kwargs.get('image'): - image = kwargs.get('image') + profile = get_container_profile(copy.deepcopy(kwargs.get('profile'))) + kw_overrides = copy.deepcopy(kwargs) + + def select(key, default=None): + kw_overrides_match = kw_overrides.pop(key, _marker) + profile_match = profile.pop(key, default) + # let kwarg overrides be the preferred choice + if kw_overrides_match is _marker: + return profile_match + return kw_overrides_match + + cntrs = ls() + image = select('image') + vgname = select('vgname') + template = select('template') + if image: proto = _urlparse(image).scheme img_tar = __salt__['cp.cache_file'](image) img_name = os.path.basename(img_tar) @@ -739,31 +737,28 @@ def get_base(**kwargs): name = '__base_{0}_{1}_{2}'.format(proto, img_name, hash_) if name not in cntrs: create(name, **kwargs) - if kwargs.get('vgname'): - rootfs = os.path.join('/dev', kwargs['vgname'], name) - lxc_info = info(name) - edit_conf(lxc_info['config'], **{'lxc.rootfs': rootfs}) + if vgname: + rootfs = os.path.join('/dev', vgname, name) + edit_conf(info(name)['config'], **{'lxc.rootfs': rootfs}) return name - elif kwargs.get('template'): + elif template: name = '__base_{0}'.format(kwargs['template']) if name not in cntrs: create(name, **kwargs) - if kwargs.get('vgname'): - rootfs = os.path.join('/dev', kwargs['vgname'], name) - lxc_info = info(name) - edit_conf(lxc_info['config'], **{'lxc.rootfs': rootfs}) + if vgname: + rootfs = os.path.join('/dev', vgname, name) + edit_conf(info(name)['config'], **{'lxc.rootfs': rootfs}) return name return '' def init(name, - image=None, config=None, cpuset=None, cpushare=None, memory=None, - nic=None, profile=None, + network_profile=None, nic_opts=None, cpu=None, autostart=True, @@ -778,6 +773,7 @@ def init(name, priv_key=None, force_install=False, unconditional_install=False, + bootstrap_delay=None, bootstrap_args=None, bootstrap_shell=None, bootstrap_url=None, @@ -789,8 +785,24 @@ def init(name, will reset a bit the lxc configuration file but much of the hard work will be escaped as markers will prevent re-execution of harmful tasks. + CLI Example: + + .. code-block:: bash + + salt 'minion' lxc.init name [cpuset=cgroups_cpuset] \\ + [cpushare=cgroups_cpushare] [memory=cgroups_memory] \\ + [network_profile=network_profile] [profile=lxc_profile] \\ + [nic_opts=nic_opts] [start=(True|False)] \\ + [seed=(True|False)] [install=(True|False)] \\ + [config=minion_config] [approve_key=(True|False) \\ + [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. + Name of the container image A tar archive to use as the rootfs for the container. Conflicts with @@ -820,11 +832,11 @@ def init(name, nic_opts Extra options for network interfaces, will override - ``{"eth0": {"hwaddr": "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": {"hwaddr": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1/24", "ipv6": "2001:db8::ff00:42:8329"}}`` + ``{"eth0": {"mac": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1/24", "ipv6": "2001:db8::ff00:42:8329"}}`` users Sysadmins users to set the administrative password to @@ -861,8 +873,16 @@ def init(name, clone Original from which to use a clone operation to create the container. Default: ``None`` + + bootstrap_delay + Delay in seconds between end of container creation and bootstrapping. + Useful when waiting for container to obtain a DHCP lease. + + .. versionadded:: 2014.7.1 + bootstrap_url See lxc.bootstrap + bootstrap_shell See lxc.bootstrap bootstrap_args @@ -892,12 +912,13 @@ def init(name, ''' ret = {'name': name, - 'result': False} + 'result': False, + 'changes': {}} # Changes is a pointer to changes_dict['init']. This method is used so that # we can have a list of changes as they are made, providing an ordered list # of things that were changed. - changes_dict = {'init', {}) + changes_dict = {'init': []} changes = changes_dict.get('init') state_pre = state(name) @@ -905,10 +926,7 @@ def init(name, if users is None: users = [] dusers = ['root'] - if ( - __grains__['os'] in ['Ubuntu'] - and 'ubuntu' not in users - ): + if __grains__['os'] in ['Ubuntu'] and 'ubuntu' not in users: dusers.append('ubuntu') for user in dusers: if user not in users: @@ -938,13 +956,19 @@ def init(name, # If using a volume group then set up to make snapshot cow clones if vgname and not clone_from: - clone_from = get_base(vgname=vgname, **kwargs) + try: + clone_from = _get_base(vgname=vgname, profile=profile, **kwargs) + except (SaltInvocationError, CommandExecutionError) as exc: + ret['comment'] = exc.message + if changes: + ret['changes'] = changes_dict + return ret if not kwargs.get('snapshot') is False: kwargs['snapshot'] = True does_exist = exists(name) to_reboot = False remove_seed_marker = False - elif clone_from: + if clone_from: remove_seed_marker = True try: clone(name, clone_from, profile=profile, **kwargs) @@ -954,9 +978,9 @@ def init(name, ret['changes'] = changes_dict return ret changes.append({'create': 'Container cloned'}) - cfg = _LXCConfig(name=name, nic=nic, nic_opts=nic_opts, - bridge=bridge, gateway=gateway, - autostart=autostart, + cfg = _LXCConfig(name=name, network_profile=network_profile, + nic_opts=nic_opts, bridge=bridge, + gateway=gateway, autostart=autostart, cpuset=cpuset, cpushare=cpushare, memory=memory) old_chunks = read_conf(cfg.path) cfg.write() @@ -965,7 +989,8 @@ def init(name, to_reboot = True else: remove_seed_marker = True - cfg = _LXCConfig(nic=nic, nic_opts=nic_opts, cpuset=cpuset, + cfg = _LXCConfig(network_profile=network_profile, + nic_opts=nic_opts, cpuset=cpuset, bridge=bridge, gateway=gateway, autostart=autostart, cpushare=cpushare, memory=memory) @@ -984,7 +1009,8 @@ def init(name, old_chunks = read_conf(path) for comp in _config_list(conf_tuples=old_chunks, cpu=cpu, - nic=nic, nic_opts=nic_opts, bridge=bridge, + network_profile=network_profile, + nic_opts=nic_opts, bridge=bridge, cpuset=cpuset, cpushare=cpushare, memory=memory): edit_conf(path, **comp) @@ -995,9 +1021,9 @@ def init(name, cmd_run(name, 'rm -f \'{0}\''.format(SEED_MARKER), python_shell=True) # 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, + cfg = _LXCConfig(name=name, network_profile=network_profile, + nic_opts=nic_opts, bridge=bridge, + gateway=gateway, autostart=autostart, cpuset=cpuset, cpushare=cpushare, memory=memory) old_chunks = [] if os.path.exists(cfg.path): @@ -1076,7 +1102,7 @@ def init(name, return ret changes.append({'dns': 'DNS updated'}) if cmd_retcode(name, - 'sh -c \'touch "{0}"; test -e "{0}"'.format(gid), + 'sh -c \'touch "{0}"; test -e "{0}"\''.format(gid), ignore_retcode=True) != 0: ret['comment'] = 'Failed to set DNS marker' if changes: @@ -1086,6 +1112,13 @@ def init(name, if seed or seed_cmd: if seed: try: + if bootstrap_delay is not None: + try: + time.sleep(bootstrap_delay) + except TypeError: + # Bad input, but assume since a value was passed that + # a delay was desired, and sleep for 5 seconds + time.sleep(5) result = bootstrap( name, config=salt_config, approve_key=approve_key, @@ -1173,10 +1206,59 @@ def cloud_init(name, vm_=None, **kwargs): return init(name, **init_interface) +def images(dist=None): + ''' + .. versionadded:: 2014.7.1 + + List the available images for LXC's ``download`` template. + + dist : None + Filter results to a single Linux distribution + + CLI Examples: + + .. code-block:: bash + + salt myminion lxc.images + salt myminion lxc.images dist=centos + ''' + out = __salt__['cmd.run_stdout']( + 'lxc-create -n __imgcheck -t download -- --list', + ignore_retcode=True + ) + if 'DIST' not in out: + raise CommandExecutionError( + 'Unable to run the \'download\' template script. Is it installed?' + ) + + ret = {} + passed_header = False + for line in out.splitlines(): + try: + distro, release, arch, variant, build_time = line.split() + except ValueError: + continue + + if not passed_header and dist == 'DIST': + passed_header = True + continue + + dist_list = ret.setdefault(distro, []) + dist_list.append({ + 'release': release, + 'arch': arch, + 'variant': variant, + 'build_time': build_time, + }) + + if dist is not None: + return dict([(dist, ret.get(dist, []))]) + return ret + + def create(name, config=None, profile=None, - options=None, **kwargs): ''' Create a new container. @@ -1184,6 +1266,10 @@ def create(name, name Name of the container + config + The config file to use for the container. Defaults to system-wide + config (usually in /etc/lxc/lxc.conf). + profile Profile to use in container creation (see :mod:`lxc.get_container_profile @@ -1192,16 +1278,30 @@ def create(name, **Container Creation Arguments** - config - The config file to use for the container. Defaults to system-wide - config (usually in /etc/lxc/lxc.conf). - template The template to use. E.g., 'ubuntu' or 'fedora'. Conflicts with the ``image`` argument. + .. note:: + + The ``download`` template requires the following three parameters + to be defined in ``options``: + + * **dist** - The name of the distribution + * **release** - Release name/version + * **arch** - Architecture of the container + + The available images can be listed using the :mod:`lxc.images + ` function. + options - Template-specific options to pass to the lxc-create command + Template-specific options to pass to the lxc-create command. These + correspond to the long options (ones beginning with two dashes) that + the template script accepts. For example: + + .. code-block:: bash + + options='{"dist": "centos", "release": "6", "arch": "amd64"}' image A tar archive to use as the rootfs for the container. Conflicts with @@ -1230,6 +1330,9 @@ def create(name, 'Container \'{0}\' already exists'.format(name) ) + # Required params for 'download' template + download_template_deps = ('dist', 'release', 'arch') + cmd = 'lxc-create -n {0}'.format(name) profile = get_container_profile(copy.deepcopy(profile)) @@ -1254,6 +1357,7 @@ def create(name, 'Only one of \'template\' and \'image\' is permitted' ) + options = select('options') backing = select('backing') if vgname and not backing: backing = 'lvm' @@ -1292,12 +1396,16 @@ def create(name, cmd += ' --fstype {0}'.format(fstype) if size: cmd += ' --fssize {0}'.format(size) - options = options or {} - if profile: - profile.update(options) - options = profile if options: + if template == 'download': + missing_deps = [x for x in download_template_deps + if x not in options] + if missing_deps: + raise SaltInvocationError( + 'Missing params in \'options\' dict: {0}' + .format(', '.join(missing_deps)) + ) cmd += ' --' for key, val in options.items(): cmd += ' --{0} {1}'.format(key, val) @@ -1495,12 +1603,7 @@ def list_(extra=False, limit=None): continue if extra: - try: - infos = info(container) - except Exception: - trace = traceback.format_exc() - infos = {'error': 'Error while getting extra infos', - 'comment': trace} + infos = info(container) method = 'update' value = {container: infos} else: @@ -1601,7 +1704,7 @@ def start(name, restart=False): ''' _ensure_exists(name) if restart: - __salt__['lxc.stop'](name) + stop(name) return _change_state('lxc-start -d', name, 'running') @@ -1930,10 +2033,25 @@ def info(name): def set_password(name, users, password, encrypted=True): ''' .. versionchanged:: 2014.7.1 - Function renamed from ``set_pass`` to ``set_password``. + Function renamed from ``set_pass`` to ``set_password``. Additionally, + this function now supports (and defaults to using) a password hash + instead of a plaintext password. Set the password of one or more system users inside containers + + users + Comma-separated list (or python list) of users to change password + + password + Password to set for the specified user(s) + + encrypted : True + If true, ``password`` must be a password hash. Set to ``False`` to set + a plaintext password (not recommended). + + .. versionadded:: 2014.7.1 + CLI Example: .. code-block:: bash @@ -1973,7 +2091,8 @@ set_pass = set_password def update_lxc_conf(name, lxc_conf, lxc_conf_unset): - '''Edit LXC configuration options + ''' + Edit LXC configuration options CLI Example: @@ -1984,6 +2103,13 @@ def update_lxc_conf(name, lxc_conf, lxc_conf_unset): lxc_conf_unset="['lxc.utsname']" ''' + _ensure_exists(name) + lxc_conf_p = '/var/lib/lxc/{0}/config'.format(name) + if not os.path.exists(lxc_conf_p): + raise SaltInvocationError( + 'Configuration file {0} does not exist'.format(lxc_conf_p) + ) + changes = {'edited': [], 'added': [], 'removed': []} ret = {'changes': changes, 'result': True, 'comment': ''} lxc_conf_p = '/var/lib/lxc/{0}/config'.format(name) @@ -2045,28 +2171,27 @@ def update_lxc_conf(name, lxc_conf, lxc_conf_unset): dest_lxc_conf.append(line) else: changes['removed'].append(opt) + else: + dest_lxc_conf = lines + conf = '' + for key, val in dest_lxc_conf: + if not val: + conf += '{0}\n'.format(key) else: - dest_lxc_conf = lines - conf = '' - for k, val in dest_lxc_conf: - if not val: - conf += '{0}\n'.format(k) - else: - conf += '{0} = {1}\n'.format(k.strip(), val.strip()) - conf_changed = conf != orig_config - chrono = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') - if conf_changed: - with salt.utils.fopen('{0}.{1}'.format(lxc_conf_p, chrono), 'w') as wfic: - wfic.write(conf) - with salt.utils.fopen(lxc_conf_p, 'w') as wfic: - wfic.write(conf) - ret['comment'] = 'Updated' - ret['result'] = True - if ( - not changes['added'] - and not changes['edited'] - and not changes['removed'] - ): + conf += '{0} = {1}\n'.format(key.strip(), val.strip()) + conf_changed = conf != orig_config + chrono = datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S') + if conf_changed: + path = '.'.join((lxc_conf_p, chrono)) + with salt.utils.fopen(path, 'w') as wfic: + wfic.write(conf) + with salt.utils.fopen(lxc_conf_p, 'w') as wfic: + wfic.write(conf) + ret['comment'] = 'Updated' + ret['result'] = True + + if not any(changes[x] for x in changes): + # Ensure an empty changes dict if nothing was modified ret['changes'] = {} return ret @@ -2182,9 +2307,10 @@ def bootstrap(name, c_info = info(name) if not c_info: return None - # default set here as we cannot set them - # in def as it can come from a chain of procedures. - if not bootstrap_args: + + if bootstrap_args: + bootstrap_args = '{0} -c {{0}}'.format(bootstrap_args) + else: bootstrap_args = '-c {0}' if not bootstrap_shell: bootstrap_shell = 'sh' @@ -2994,7 +3120,7 @@ def write_conf(conf_file, conf): {'lxc.network.type': 'veth'}, {'lxc.network.flags': 'up'}, {'lxc.network.link': 'br0'}, - {'lxc.network.hwaddr': '$CONTAINER_MACADDR'}, + {'lxc.network.mac': '$CONTAINER_MACADDR'}, {'lxc.network.ipv4': '$CONTAINER_IPADDR'}, {'lxc.network.name': '$CONTAINER_DEVICENAME'}, ] @@ -3007,7 +3133,7 @@ def write_conf(conf_file, conf): out_format=commented ''' if not isinstance(conf, list): - return {'Error': 'conf must be passed in as a list'} + raise SaltInvocationError('Configuration must be passed as a list') with salt.utils.fopen(conf_file, 'w') as fp_: for line in conf: