­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ """ Control Linux Containers via Salt :depends: lxc package for distribution lxc >= 1.0 (even beta alpha) is required """ import copy import datetime import difflib import logging import os import random import re import shlex import shutil import string import tempfile import textwrap import time import urllib.parse from collections import OrderedDict import salt.config import salt.utils.args import salt.utils.cloud import salt.utils.data import salt.utils.dictupdate import salt.utils.files import salt.utils.functools import salt.utils.hashutils import salt.utils.network import salt.utils.path import salt.utils.stringutils from salt.exceptions import CommandExecutionError, SaltInvocationError from salt.utils.versions import Version # Set up logging log = logging.getLogger(__name__) # Don't shadow built-in's. __func_alias__ = {"list_": "list", "ls_": "ls"} __virtualname__ = "lxc" DEFAULT_NIC = "eth0" DEFAULT_BR = "br0" SEED_MARKER = "/lxc.initial_seed" EXEC_DRIVER = "lxc-attach" DEFAULT_PATH = "/var/lib/lxc" _marker = object() def __virtual__(): if salt.utils.path.which("lxc-start"): return __virtualname__ # To speed up the whole thing, we decided to not use the # subshell way and assume things are in place for lxc # Discussion made by @kiorky and @thatch45 # lxc-version presence is not sufficient, in lxc1.0 alpha # (precise backports), we have it and it is sufficient # for the module to execute. # elif salt.utils.path.which('lxc-version'): # passed = False # try: # passed = subprocess.check_output( # 'lxc-version').split(':')[1].strip() >= '1.0' # except Exception: # pylint: disable=broad-except # pass # if not passed: # log.warning('Support for lxc < 1.0 may be incomplete.') # return 'lxc' # return False # return ( False, "The lxc execution module cannot be loaded: the lxc-start binary is not in the" " path.", ) def get_root_path(path): """ Get the configured lxc root for containers .. versionadded:: 2015.8.0 CLI Example: .. code-block:: bash salt '*' lxc.get_root_path """ if not path: path = __opts__.get("lxc.root_path", DEFAULT_PATH) return path def version(): """ Return the actual lxc client version .. versionadded:: 2015.8.0 CLI Example: .. code-block:: bash salt '*' lxc.version """ k = "lxc.version" if not __context__.get(k, None): cversion = __salt__["cmd.run_all"]("lxc-info --version") if not cversion["retcode"]: ver = Version(cversion["stdout"]) if ver < Version("1.0"): raise CommandExecutionError("LXC should be at least 1.0") __context__[k] = f"{ver}" return __context__.get(k, None) def _clear_context(): """ Clear any lxc variables set in __context__ """ for var in [x for x in __context__ if x.startswith("lxc.")]: log.trace("Clearing __context__['%s']", var) __context__.pop(var, None) def _ip_sort(ip): """Ip sorting""" idx = "001" if ip == "127.0.0.1": idx = "200" if ip == "::1": idx = "201" elif "::" in ip: idx = "100" return f"{idx}___{ip}" def search_lxc_bridges(): """ Search which bridges are potentially available as LXC bridges CLI Example: .. code-block:: bash salt '*' lxc.search_lxc_bridges """ bridges = __context__.get("lxc.bridges", None) # either match not yet called or no bridges were found # to handle the case where lxc was not installed on the first # call if not bridges: bridges = set() running_bridges = set() bridges.add(DEFAULT_BR) try: output = __salt__["cmd.run_all"]("brctl show") for line in output["stdout"].splitlines()[1:]: if not line.startswith(" "): running_bridges.add(line.split()[0].strip()) except (SaltInvocationError, CommandExecutionError): pass for ifc, ip in __grains__.get("ip_interfaces", {}).items(): if ifc in running_bridges: bridges.add(ifc) elif os.path.exists(f"/sys/devices/virtual/net/{ifc}/bridge"): bridges.add(ifc) bridges = list(bridges) # if we found interfaces that have lxc in their names # we filter them as being the potential lxc bridges # we also try to default on br0 on other cases def sort_bridges(a): pref = "z" if "lxc" in a: pref = "a" elif "br0" == a: pref = "c" return f"{pref}_{a}" bridges.sort(key=sort_bridges) __context__["lxc.bridges"] = bridges return bridges def search_lxc_bridge(): """ Search the first bridge which is potentially available as LXC bridge CLI Example: .. code-block:: bash salt '*' lxc.search_lxc_bridge """ return search_lxc_bridges()[0] def _get_salt_config(config, **kwargs): if not config: config = kwargs.get("minion", {}) if not config: config = {} config.setdefault( "master", kwargs.get("master", __opts__.get("master", __opts__["id"])) ) config.setdefault( "master_port", kwargs.get( "master_port", __opts__.get("master_port", __opts__.get("ret_port", __opts__.get("4506"))), ), ) if not config["master"]: config = {} return config 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 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 path path to the container parent directory (default: /var/lib/lxc) .. versionadded:: 2015.8.0 profile :ref:`profile ` selection network_profile :ref:`network profile ` selection nic_opts per interface settings compatibles with network profile (ipv4/ipv6/link/gateway/mac/netmask) eg:: - {'eth0': {'mac': '00:16:3e:01:29:40', 'gateway': None, (default) 'link': 'br0', (default) 'gateway': None, (default) 'netmask': '', (default) 'ip': '22.1.4.25'}} unconditional_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 list of DNS servers to set inside the container dns_via_dhcp do not set the dns servers, let them be set by the dhcp. autostart autostart the container at boot time password administrative password for the container bootstrap_delay delay before launching bootstrap script at Container init .. warning:: Legacy but still supported options: 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 lvname if using LVM: lvname thinpool: if using LVM: thinpool ip ip for the primary nic mac mac address 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 additional_ips 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 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'} users administrative users for the container default: [root] and [root, ubuntu] on ubuntu default_nic name of the first interface, you should really not override this CLI Example: .. code-block:: bash salt '*' lxc.cloud_init_interface foo """ if vm_ is None: vm_ = {} vm_ = copy.deepcopy(vm_) vm_ = salt.utils.dictupdate.update(vm_, kwargs) profile_data = copy.deepcopy(vm_.get("lxc_profile", vm_.get("profile", {}))) if not isinstance(profile_data, (dict, (str,))): profile_data = {} profile = get_container_profile(profile_data) def _cloud_get(k, default=None): return vm_.get(k, profile.get(k, default)) if name is None: name = vm_["name"] # if we are on ubuntu, default to ubuntu default_template = "" if __grains__.get("os", "") in ["Ubuntu"]: default_template = "ubuntu" image = _cloud_get("image") if not image: _cloud_get("template", default_template) backing = _cloud_get("backing", "dir") if image: profile["template"] = image vgname = _cloud_get("vgname", None) if vgname: profile["vgname"] = vgname if backing: profile["backing"] = backing snapshot = _cloud_get("snapshot", False) autostart = bool(_cloud_get("autostart", True)) dnsservers = _cloud_get("dnsservers", []) dns_via_dhcp = _cloud_get("dns_via_dhcp", True) password = _cloud_get("password", "s3cr3t") password_encrypted = _cloud_get("password_encrypted", False) fstype = _cloud_get("fstype", None) lvname = _cloud_get("lvname", None) thinpool = _cloud_get("thinpool", None) pub_key = _cloud_get("pub_key", None) priv_key = _cloud_get("priv_key", None) size = _cloud_get("size", "20G") script = _cloud_get("script", None) script_args = _cloud_get("script_args", None) users = _cloud_get("users", None) if users is None: users = [] ssh_username = _cloud_get("ssh_username", None) if ssh_username and (ssh_username not in users): users.append(ssh_username) network_profile = _cloud_get("network_profile", None) nic_opts = kwargs.get("nic_opts", None) netmask = _cloud_get("netmask", "24") path = _cloud_get("path", None) bridge = _cloud_get("bridge", None) gateway = _cloud_get("gateway", None) unconditional_install = _cloud_get("unconditional_install", False) force_install = _cloud_get("force_install", True) config = _get_salt_config(_cloud_get("config", {}), **vm_) default_nic = _cloud_get("default_nic", DEFAULT_NIC) # do the interface with lxc.init mainly via nic_opts # to avoid extra and confusing extra use cases. if not isinstance(nic_opts, dict): nic_opts = OrderedDict() # have a reference to the default nic eth0 = nic_opts.setdefault(default_nic, OrderedDict()) # lxc config is based of ifc order, be sure to use odicts. if not isinstance(nic_opts, OrderedDict): bnic_opts = OrderedDict() bnic_opts.update(nic_opts) nic_opts = bnic_opts gw = None # legacy salt.cloud scheme for network interfaces settings support bridge = _cloud_get("bridge", None) ip = _cloud_get("ip", None) mac = _cloud_get("mac", None) if ip: fullip = ip if netmask: fullip += f"/{netmask}" eth0["ipv4"] = fullip if mac is not None: eth0["mac"] = mac for ix, iopts in enumerate(_cloud_get("additional_ips", [])): ifh = f"eth{ix + 1}" ethx = nic_opts.setdefault(ifh, {}) if gw is None: gw = iopts.get("gateway", ethx.get("gateway", None)) if gw: # only one and only one default gateway is allowed ! eth0.pop("gateway", None) gateway = None # even if the gateway if on default "eth0" nic # and we popped it will work # as we reinject or set it here. ethx["gateway"] = gw elink = iopts.get("link", ethx.get("link", None)) if elink: ethx["link"] = elink # allow dhcp aip = iopts.get("ipv4", iopts.get("ip", None)) if aip: ethx["ipv4"] = aip nm = iopts.get("netmask", "") if nm: ethx["ipv4"] += f"/{nm}" for i in ("mac", "hwaddr"): if i in iopts: ethx["mac"] = iopts[i] break if "mac" not in ethx: ethx["mac"] = salt.utils.network.gen_mac() # last round checking for unique gateway and such gw = None for ethx in [a for a in nic_opts]: ndata = nic_opts[ethx] if gw: ndata.pop("gateway", None) if "gateway" in ndata: gw = ndata["gateway"] gateway = None # only use a default bridge / gateway if we configured them # via the legacy salt cloud configuration style. # On other cases, we should rely on settings provided by the new # salt lxc network profile style configuration which can # be also be overridden or a per interface basis via the nic_opts dict. if bridge: eth0["link"] = bridge if gateway: eth0["gateway"] = gateway # lxc_init_interface = {} lxc_init_interface["name"] = name lxc_init_interface["config"] = config lxc_init_interface["memory"] = _cloud_get("memory", 0) # nolimit lxc_init_interface["pub_key"] = pub_key lxc_init_interface["priv_key"] = priv_key lxc_init_interface["nic_opts"] = nic_opts for clone_from in ["clone_from", "clone", "from_container"]: # clone_from should default to None if not available lxc_init_interface["clone_from"] = _cloud_get(clone_from, None) if lxc_init_interface["clone_from"] is not None: break lxc_init_interface["profile"] = profile lxc_init_interface["snapshot"] = snapshot lxc_init_interface["dnsservers"] = dnsservers lxc_init_interface["fstype"] = fstype lxc_init_interface["path"] = path lxc_init_interface["vgname"] = vgname lxc_init_interface["size"] = size lxc_init_interface["lvname"] = lvname lxc_init_interface["thinpool"] = thinpool lxc_init_interface["force_install"] = force_install lxc_init_interface["unconditional_install"] = unconditional_install lxc_init_interface["bootstrap_url"] = script lxc_init_interface["bootstrap_args"] = script_args lxc_init_interface["bootstrap_shell"] = _cloud_get("bootstrap_shell", "sh") lxc_init_interface["bootstrap_delay"] = _cloud_get("bootstrap_delay", None) lxc_init_interface["autostart"] = autostart lxc_init_interface["users"] = users lxc_init_interface["password"] = password lxc_init_interface["password_encrypted"] = password_encrypted # be sure not to let objects goes inside the return # as this return will be msgpacked for use in the runner ! lxc_init_interface["network_profile"] = network_profile for i in ["cpu", "cpuset", "cpushare"]: if _cloud_get(i, None): try: lxc_init_interface[i] = vm_[i] except KeyError: lxc_init_interface[i] = profile[i] return lxc_init_interface def _get_profile(key, name, **kwargs): if isinstance(name, dict): profilename = name.pop("name", None) return _get_profile(key, profilename, **name) if name is None: profile_match = {} else: profile_match = __salt__["config.get"]( f"lxc.{key}:{name}", default=None, merge="recurse" ) if profile_match is None: # No matching profile, make the profile an empty dict so that # overrides can be applied below. profile_match = {} if not isinstance(profile_match, dict): raise CommandExecutionError(f"lxc.{key} must be a dictionary") # Overlay the kwargs to override matched profile data overrides = salt.utils.args.clean_kwargs(**copy.deepcopy(kwargs)) profile_match = salt.utils.dictupdate.update( copy.deepcopy(profile_match), overrides ) return profile_match def get_container_profile(name=None, **kwargs): """ .. versionadded:: 2015.5.0 Gather a pre-configured set of container configuration parameters. If no arguments are passed, an empty profile is returned. Profiles can be defined in the minion or master config files, or in pillar or grains, and are loaded using :mod:`config.get `. The key under which LXC profiles must be configured is ``lxc.container_profile.profile_name``. An example container profile would be as follows: .. code-block:: yaml lxc.container_profile: ubuntu: template: ubuntu backing: lvm vgname: lxc size: 1G Parameters set in a profile can be overridden by passing additional container creation arguments (such as the ones passed to :mod:`lxc.create `) to this function. A profile can be defined either as the name of the profile, or a dictionary of variable names and values. See the :ref:`LXC Tutorial ` for more information on how to use LXC profiles. CLI Example: .. code-block:: bash salt-call lxc.get_container_profile centos salt-call lxc.get_container_profile ubuntu template=ubuntu backing=overlayfs """ profile = _get_profile("container_profile", name, **kwargs) return profile def get_network_profile(name=None, **kwargs): """ .. versionadded:: 2015.5.0 Gather a pre-configured set of network configuration parameters. If no arguments are passed, the following default profile is returned: .. code-block:: python {'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 `. The key under which LXC profiles must be configured is ``lxc.network_profile``. An example network profile would be as follows: .. code-block:: yaml lxc.network_profile.centos: eth0: link: br0 type: veth flags: up To disable networking entirely: .. code-block:: yaml lxc.network_profile.centos: eth0: disable: true Parameters set in a profile can be overridden by passing additional arguments to this function. A profile can be passed either as the name of the profile, or a dictionary of variable names and values. See the :ref:`LXC Tutorial ` for more information on how to use network profiles. .. warning:: The ``ipv4``, ``ipv6``, ``gateway``, and ``link`` (bridge) settings in network profiles will only work if the container doesn't redefine the network configuration (for example in ``/etc/sysconfig/network-scripts/ifcfg-`` on RHEL/CentOS, or ``/etc/network/interfaces`` on Debian/Ubuntu/etc.) CLI Example: .. code-block:: bash salt-call lxc.get_network_profile default """ profile = _get_profile("network_profile", name, **kwargs) return profile def _rand_cpu_str(cpu): """ Return a random subset of cpus for the cpuset config """ cpu = int(cpu) avail = __salt__["status.nproc"]() if cpu < avail: return f"0-{avail}" to_set = set() while len(to_set) < cpu: choice = random.randint(0, avail - 1) if choice not in to_set: to_set.add(str(choice)) return ",".join(sorted(to_set)) def _network_conf(conf_tuples=None, **kwargs): """ Network configuration defaults network_profile as for containers, we can either call this function either with a network_profile dict or network profile name in the kwargs nic_opts overrides or extra nics in the form {nic_name: {set: tings} """ nic = kwargs.get("network_profile", None) ret = [] nic_opts = kwargs.get("nic_opts", {}) if nic_opts is None: # coming from elsewhere nic_opts = {} if not conf_tuples: conf_tuples = [] old = _get_veths(conf_tuples) if not old: old = {} # if we have a profile name, get the profile and load the network settings # this will obviously by default look for a profile called "eth0" # or by what is defined in nic_opts # and complete each nic settings by sane defaults if nic and isinstance(nic, ((str,), dict)): nicp = get_network_profile(nic) else: nicp = {} if DEFAULT_NIC not in nicp: nicp[DEFAULT_NIC] = {} kwargs = copy.deepcopy(kwargs) gateway = kwargs.pop("gateway", None) bridge = kwargs.get("bridge", None) if nic_opts: for dev, args in nic_opts.items(): ethx = nicp.setdefault(dev, {}) try: ethx = salt.utils.dictupdate.update(ethx, args) except AttributeError: raise SaltInvocationError("Invalid nic_opts configuration") ifs = [a for a in nicp] ifs += [a for a in old if a not in nicp] ifs.sort() gateway_set = False for dev in ifs: args = nicp.get(dev, {}) opts = nic_opts.get(dev, {}) if nic_opts else {} old_if = old.get(dev, {}) disable = opts.get("disable", args.get("disable", False)) if disable: continue mac = opts.get( "mac", opts.get("hwaddr", args.get("mac", args.get("hwaddr", ""))) ) type_ = opts.get("type", args.get("type", "")) flags = opts.get("flags", args.get("flags", "")) link = opts.get("link", args.get("link", "")) ipv4 = opts.get("ipv4", args.get("ipv4", "")) ipv6 = opts.get("ipv6", args.get("ipv6", "")) infos = OrderedDict( [ ( "lxc.network.type", { "test": not type_, "value": type_, "old": old_if.get("lxc.network.type"), "default": "veth", }, ), ( "lxc.network.name", {"test": False, "value": dev, "old": dev, "default": dev}, ), ( "lxc.network.flags", { "test": not flags, "value": flags, "old": old_if.get("lxc.network.flags"), "default": "up", }, ), ( "lxc.network.link", { "test": not link, "value": link, "old": old_if.get("lxc.network.link"), "default": search_lxc_bridge(), }, ), ( "lxc.network.hwaddr", { "test": not mac, "value": mac, "old": old_if.get("lxc.network.hwaddr"), "default": salt.utils.network.gen_mac(), }, ), ( "lxc.network.ipv4", { "test": not ipv4, "value": ipv4, "old": old_if.get("lxc.network.ipv4", ""), "default": None, }, ), ( "lxc.network.ipv6", { "test": not ipv6, "value": ipv6, "old": old_if.get("lxc.network.ipv6", ""), "default": None, }, ), ] ) # for each parameter, if not explicitly set, the # config value present in the LXC configuration should # take precedence over the profile configuration for info in list(infos.keys()): bundle = infos[info] if bundle["test"]: if bundle["old"]: bundle["value"] = bundle["old"] elif bundle["default"]: bundle["value"] = bundle["default"] for info, data in infos.items(): if data["value"]: ret.append({info: data["value"]}) for key, val in args.items(): if key == "link" and bridge: val = bridge val = opts.get(key, val) if key in [ "type", "flags", "name", "gateway", "mac", "link", "ipv4", "ipv6", ]: continue ret.append({f"lxc.network.{key}": val}) # gateway (in automode) must be appended following network conf ! if not gateway: gateway = args.get("gateway", None) if gateway is not None and not gateway_set: ret.append({"lxc.network.ipv4.gateway": gateway}) # only one network gateway ;) gateway_set = True # normally, this won't happen # set the gateway if specified even if we did # not managed the network underlying if gateway is not None and not gateway_set: ret.append({"lxc.network.ipv4.gateway": gateway}) # only one network gateway ;) gateway_set = True new = _get_veths(ret) # verify that we did not loose the mac settings for iface in [a for a in new]: ndata = new[iface] nmac = ndata.get("lxc.network.hwaddr", "") ntype = ndata.get("lxc.network.type", "") omac, otype = "", "" if iface in old: odata = old[iface] omac = odata.get("lxc.network.hwaddr", "") otype = odata.get("lxc.network.type", "") # default for network type is setted here # attention not to change the network type # without a good and explicit reason to. if otype and not ntype: ntype = otype if not ntype: ntype = "veth" new[iface]["lxc.network.type"] = ntype if omac and not nmac: new[iface]["lxc.network.hwaddr"] = omac ret = [] for val in new.values(): for row in val: ret.append(OrderedDict([(row, val[row])])) # on old versions of lxc, still support the gateway auto mode # if we didn't explicitly say no to # (lxc.network.ipv4.gateway: auto) if ( Version(version()) <= Version("1.0.7") and True not in ["lxc.network.ipv4.gateway" in a for a in ret] and True in ["lxc.network.ipv4" in a for a in ret] ): ret.append({"lxc.network.ipv4.gateway": "auto"}) return ret def _get_lxc_default_data(**kwargs): kwargs = copy.deepcopy(kwargs) ret = {} for k in ["utsname", "rootfs"]: val = kwargs.get(k, None) if val is not None: ret[f"lxc.{k}"] = val autostart = kwargs.get("autostart") # autostart can have made in kwargs, but with the None # value which is invalid, we need an explicit boolean # autostart = on is the default. if autostart is None: autostart = True # we will set the regular lxc marker to restart container at # machine (re)boot only if we did not explicitly ask # not to touch to the autostart settings via # autostart == 'keep' if autostart != "keep": if autostart: ret["lxc.start.auto"] = "1" else: ret["lxc.start.auto"] = "0" memory = kwargs.get("memory") if memory is not None: # converting the config value from MB to bytes ret["lxc.cgroup.memory.limit_in_bytes"] = memory * 1024 * 1024 cpuset = kwargs.get("cpuset") if cpuset: ret["lxc.cgroup.cpuset.cpus"] = cpuset cpushare = kwargs.get("cpushare") cpu = kwargs.get("cpu") 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, only_net=False, **kwargs): """ Return a list of dicts from the salt level configurations conf_tuples _LXCConfig compatible list of entries which can contain - string line - tuple (lxc config param,value) - dict of one entry: {lxc config param: value) only_net by default we add to the tuples a reflection of both the real config if avalaible and a certain amount of default values like the cpu parameters, the memory and etc. On the other hand, we also no matter the case reflect the network configuration computed from the actual config if available and given values. if no_default_loads is set, we will only reflect the network configuration back to the conf tuples list """ # explicit cast only_net = bool(only_net) if not conf_tuples: conf_tuples = [] kwargs = copy.deepcopy(kwargs) ret = [] if not only_net: default_data = _get_lxc_default_data(**kwargs) for k, val in default_data.items(): ret.append({k: val}) net_datas = _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 dictionary indexed by network interface """ if isinstance(net_data, dict): net_data = list(net_data.items()) nics = OrderedDict() current_nic = OrderedDict() no_names = True for item in net_data: if item and isinstance(item, dict): item = list(item.items())[0] # skip LXC configuration comment lines, and play only with tuples conf elif isinstance(item, str): # deal with reflection of commented lxc configs sitem = item.strip() if sitem.startswith("#") or not sitem: continue elif "=" in item: item = tuple(a.strip() for a in item.split("=", 1)) if item[0] == "lxc.network.type": current_nic = OrderedDict() if item[0] == "lxc.network.name": no_names = False nics[item[1].strip()] = current_nic current_nic[item[0].strip()] = item[1].strip() # if not ethernet card name has been collected, assuming we collected # data for eth0 if no_names and current_nic: nics[DEFAULT_NIC] = current_nic return nics class _LXCConfig: """ 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) path = get_root_path(kwargs.get("path", None)) self.data = [] if self.name: self.path = os.path.join(path, self.name, "config") if os.path.isfile(self.path): with salt.utils.files.fopen(self.path) as fhr: for line in salt.utils.data.decode(fhr.readlines()): match = self.pattern.findall(line.strip()) if match: self.data.append((match[0][0], match[0][-1])) match = self.non_interpretable_pattern.findall(line.strip()) if match: self.data.append(("", match[0][0])) else: self.path = None def _replace(key, val): if val: self._filter_data(key) self.data.append((key, val)) default_data = _get_lxc_default_data(**kwargs) for key, val in default_data.items(): _replace(key, val) old_net = self._filter_data("lxc.network") net_datas = _network_conf(conf_tuples=old_net, **kwargs) if net_datas: for row in net_datas: self.data.extend(list(row.items())) # be sure to reset harmful settings for idx in ["lxc.cgroup.memory.limit_in_bytes"]: if not default_data.get(idx): self._filter_data(idx) def as_string(self): chunks = ( "{0[0]}{1}{0[1]}".format(item, (" = " if item[0] else "")) for item in self.data ) return "\n".join(chunks) + "\n" def write(self): if self.path: content = self.as_string() # 2 step rendering to be sure not to open/wipe the config # before as_string succeeds. with salt.utils.files.fopen(self.path, "w") as fic: fic.write(salt.utils.stringutils.to_str(content)) fic.flush() def tempfile(self): # this might look like the function name is shadowing the # module, but it's not since the method belongs to the class ntf = tempfile.NamedTemporaryFile() ntf.write(self.as_string()) ntf.flush() return ntf def _filter_data(self, pattern): """ Removes parameters which match the pattern from the config data """ removed = [] filtered = [] for param in self.data: if not param[0].startswith(pattern): filtered.append(param) else: removed.append(param) self.data = filtered return removed 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. """ 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 template = select("template") image = select("image") vgname = select("vgname") path = kwargs.get("path", None) # remove the above three variables from kwargs, if they exist, to avoid # duplicates if create() is invoked below. for param in ("path", "image", "vgname", "template"): kwargs.pop(param, None) if image: proto = urllib.parse.urlparse(image).scheme img_tar = __salt__["cp.cache_file"](image) img_name = os.path.basename(img_tar) hash_ = salt.utils.hashutils.get_hash( img_tar, __salt__["config.get"]("hash_type") ) name = f"__base_{proto}_{img_name}_{hash_}" if not exists(name, path=path): create( name, template=template, image=image, path=path, vgname=vgname, **kwargs ) if vgname: rootfs = os.path.join("/dev", vgname, name) edit_conf( info(name, path=path)["config"], out_format="commented", **{"lxc.rootfs": rootfs}, ) return name elif template: name = f"__base_{template}" if not exists(name, path=path): create( name, template=template, image=image, path=path, vgname=vgname, **kwargs ) if vgname: rootfs = os.path.join("/dev", vgname, name) edit_conf( info(name, path=path)["config"], out_format="commented", **{"lxc.rootfs": rootfs}, ) return name return "" def init( name, config=None, cpuset=None, cpushare=None, memory=None, profile=None, network_profile=None, nic_opts=None, cpu=None, autostart=True, password=None, password_encrypted=None, users=None, dnsservers=None, searchdomains=None, bridge=None, gateway=None, pub_key=None, priv_key=None, force_install=False, unconditional_install=False, bootstrap_delay=None, bootstrap_args=None, bootstrap_shell=None, bootstrap_url=None, **kwargs, ): """ Initialize a new container. This is a partial idempotent function as if it is already provisioned, 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 harmful tasks. name Name of the container image A tar archive to use as the rootfs for the container. Conflicts with the ``template`` argument. cpus Select a random number of cpu cores and assign it to the cpuset, if the cpuset option is set then this option will be ignored cpuset Explicitly define the cpus this container will be bound to cpushare cgroups cpu shares autostart autostart container on reboot memory cgroups memory limit, in MB .. versionchanged:: 2015.5.0 If no value is passed, no limit is set. In earlier Salt versions, not passing this value causes a 1024MB memory limit to be set, and it was necessary to pass ``memory=0`` to set no limit. 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 network_profile Network profile to use for the container .. versionadded:: 2015.5.0 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"}}`` or ``{"eth0": {"hwaddr": "aa:bb:cc:dd:ee:ff", "ipv4": "10.1.1.1/24", "ipv6": "2001:db8::ff00:42:8329"}}`` users Users for which the password defined in the ``password`` param should be set. Can be passed as a comma separated list or a python list. Defaults to just the ``root`` user. password Set the initial password for the users defined in the ``users`` parameter password_encrypted : False Set to ``True`` to denote a password hash instead of a plaintext password .. versionadded:: 2015.5.0 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 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`` 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. master salt master (default to minion's master) master_port salt master port (default to minion's master port) pub_key Explicit public key to preseed the minion with (optional). This can be either a filepath or a string representing the key priv_key Explicit private key to preseed the minion with (optional). 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`` path path to the container parent directory default: /var/lib/lxc (system) .. versionadded:: 2015.8.0 clone_from 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:: 2015.5.0 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 unconditional_install Run the script even if the container seems seeded CLI Example: .. code-block:: bash salt 'minion' lxc.init name [cpuset=cgroups_cpuset] \\ [cpushare=cgroups_cpushare] [memory=cgroups_memory] \\ [nic=nic_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_from=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'] \\ [password_encrypted=(True|False)] """ ret = {"name": name, "changes": {}} profile = get_container_profile(copy.deepcopy(profile)) if not network_profile: network_profile = profile.get("network_profile") if not network_profile: network_profile = DEFAULT_NIC # 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 = changes_dict.get("init") if users is None: users = [] dusers = ["root"] for user in dusers: if user not in users: users.append(user) 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 path = select("path") bpath = get_root_path(path) state_pre = state(name, path=path) tvg = select("vgname") vgname = tvg if tvg else __salt__["config.get"]("lxc.vgname") start_ = select("start", True) autostart = select("autostart", autostart) seed = select("seed", True) install = select("install", True) seed_cmd = select("seed_cmd") salt_config = _get_salt_config(config, **kwargs) approve_key = select("approve_key", True) clone_from = select("clone_from") # If using a volume group then set up to make snapshot cow clones if vgname and not clone_from: try: kwargs["vgname"] = vgname clone_from = _get_base(profile=profile, **kwargs) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = exc.strerror if changes: ret["changes"] = changes_dict return ret if not kwargs.get("snapshot") is False: kwargs["snapshot"] = True does_exist = exists(name, path=path) to_reboot = False remove_seed_marker = False if does_exist: pass elif clone_from: remove_seed_marker = True try: clone(name, clone_from, profile=profile, **kwargs) changes.append({"create": "Container cloned"}) except (SaltInvocationError, CommandExecutionError) as exc: if "already exists" in exc.strerror: changes.append({"create": "Container already exists"}) else: ret["result"] = False ret["comment"] = exc.strerror if changes: ret["changes"] = changes_dict return ret cfg = _LXCConfig( name=name, network_profile=network_profile, nic_opts=nic_opts, bridge=bridge, path=path, gateway=gateway, autostart=autostart, cpuset=cpuset, cpushare=cpushare, memory=memory, ) old_chunks = read_conf(cfg.path, out_format="commented") cfg.write() chunks = read_conf(cfg.path, out_format="commented") if old_chunks != chunks: to_reboot = True else: remove_seed_marker = True cfg = _LXCConfig( network_profile=network_profile, nic_opts=nic_opts, cpuset=cpuset, path=path, bridge=bridge, gateway=gateway, autostart=autostart, cpushare=cpushare, memory=memory, ) with cfg.tempfile() as cfile: try: create(name, config=cfile.name, profile=profile, **kwargs) changes.append({"create": "Container created"}) except (SaltInvocationError, CommandExecutionError) as exc: if "already exists" in exc.strerror: changes.append({"create": "Container already exists"}) else: ret["comment"] = exc.strerror if changes: ret["changes"] = changes_dict return ret cpath = os.path.join(bpath, name, "config") old_chunks = [] if os.path.exists(cpath): old_chunks = read_conf(cpath, out_format="commented") new_cfg = _config_list( conf_tuples=old_chunks, cpu=cpu, network_profile=network_profile, nic_opts=nic_opts, bridge=bridge, cpuset=cpuset, cpushare=cpushare, memory=memory, ) if new_cfg: edit_conf(cpath, out_format="commented", lxc_config=new_cfg) chunks = read_conf(cpath, out_format="commented") if old_chunks != chunks: to_reboot = True # last time to be sure any of our property is correctly applied cfg = _LXCConfig( name=name, network_profile=network_profile, nic_opts=nic_opts, bridge=bridge, path=path, gateway=gateway, autostart=autostart, cpuset=cpuset, cpushare=cpushare, memory=memory, ) old_chunks = [] if os.path.exists(cfg.path): old_chunks = read_conf(cfg.path, out_format="commented") cfg.write() chunks = read_conf(cfg.path, out_format="commented") if old_chunks != chunks: changes.append({"config": "Container configuration updated"}) to_reboot = True if to_reboot: try: stop(name, path=path) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = f"Unable to stop container: {exc}" if changes: ret["changes"] = changes_dict return ret if not does_exist or (does_exist and state(name, path=path) != "running"): try: start(name, path=path) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = f"Unable to stop container: {exc}" if changes: ret["changes"] = changes_dict return ret if remove_seed_marker: run( name, f"rm -f '{SEED_MARKER}'", path=path, chroot_fallback=False, python_shell=False, ) # set the default user/password, only the first time if ret.get("result", True) and password: gid = "/.lxc.initial_pass" gids = [gid, "/lxc.initial_pass", f"/.lxc.{name}.initial_pass"] if not any( retcode( name, f'test -e "{x}"', chroot_fallback=True, path=path, ignore_retcode=True, ) == 0 for x in gids ): # think to touch the default user generated by default templates # which has a really unsecure passwords... # root is defined as a member earlier in the code for default_user in ["ubuntu"]: if ( default_user not in users and retcode( name, f"id {default_user}", python_shell=False, path=path, chroot_fallback=True, ignore_retcode=True, ) == 0 ): users.append(default_user) for user in users: try: cret = set_password( name, users=[user], path=path, password=password, encrypted=password_encrypted, ) except (SaltInvocationError, CommandExecutionError) as exc: msg = f"{user}: Failed to set password" + exc.strerror # only hardfail in unrecoverable situation: # root cannot be setted up if user == "root": ret["comment"] = msg ret["result"] = False else: log.debug(msg) if ret.get("result", True): changes.append({"password": "Password(s) updated"}) if ( retcode( name, 'sh -c \'touch "{0}"; test -e "{0}"\''.format(gid), path=path, chroot_fallback=True, ignore_retcode=True, ) != 0 ): ret["comment"] = "Failed to set password marker" changes[-1]["password"] += ". " + ret["comment"] + "." ret["result"] = False # set dns servers if any, only the first time if ret.get("result", True) and dnsservers: # retro compatibility, test also old markers gid = "/.lxc.initial_dns" gids = [gid, "/lxc.initial_dns", f"/lxc.{name}.initial_dns"] if not any( retcode( name, f'test -e "{x}"', chroot_fallback=True, path=path, ignore_retcode=True, ) == 0 for x in gids ): try: set_dns( name, path=path, dnsservers=dnsservers, searchdomains=searchdomains ) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = "Failed to set DNS: " + exc.strerror ret["result"] = False else: changes.append({"dns": "DNS updated"}) if ( retcode( name, 'sh -c \'touch "{0}"; test -e "{0}"\''.format(gid), chroot_fallback=True, path=path, ignore_retcode=True, ) != 0 ): ret["comment"] = "Failed to set DNS marker" changes[-1]["dns"] += ". " + ret["comment"] + "." ret["result"] = False # retro compatibility, test also old markers if remove_seed_marker: run(name, f"rm -f '{SEED_MARKER}'", path=path, python_shell=False) gid = "/.lxc.initial_seed" gids = [gid, "/lxc.initial_seed"] if any( retcode( name, f"test -e {x}", path=path, chroot_fallback=True, ignore_retcode=True, ) == 0 for x in gids ) or not ret.get("result", True): pass elif seed or seed_cmd: if seed: try: result = bootstrap( name, config=salt_config, path=path, approve_key=approve_key, pub_key=pub_key, priv_key=priv_key, install=install, force_install=force_install, unconditional_install=unconditional_install, bootstrap_delay=bootstrap_delay, bootstrap_url=bootstrap_url, bootstrap_shell=bootstrap_shell, bootstrap_args=bootstrap_args, ) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = "Bootstrap failed: " + exc.strerror ret["result"] = False else: if not result: ret["comment"] = ( "Bootstrap failed, see minion log for more information" ) ret["result"] = False else: changes.append({"bootstrap": "Container successfully bootstrapped"}) elif seed_cmd: try: result = __salt__[seed_cmd]( info(name, path=path)["rootfs"], name, salt_config ) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = "Bootstrap via seed_cmd '{}' failed: {}".format( seed_cmd, exc.strerror ) ret["result"] = False else: if not result: ret["comment"] = ( "Bootstrap via seed_cmd '{}' failed, " "see minion log for more information ".format(seed_cmd) ) ret["result"] = False else: changes.append( { "bootstrap": ( "Container successfully bootstrapped " "using seed_cmd '{}'".format(seed_cmd) ) } ) if ret.get("result", True) and not start_: try: stop(name, path=path) except (SaltInvocationError, CommandExecutionError) as exc: ret["comment"] = f"Unable to stop container: {exc}" ret["result"] = False state_post = state(name, path=path) if state_pre != state_post: changes.append({"state": {"old": state_pre, "new": state_post}}) if ret.get("result", True): ret["comment"] = f"Container '{name}' successfully initialized" ret["result"] = True if changes: ret["changes"] = changes_dict 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 CLI Example: .. code-block:: bash salt '*' lxc.cloud_init foo """ init_interface = cloud_init_interface(name, vm_, **kwargs) name = init_interface.pop("name", name) return init(name, **init_interface) def images(dist=None): """ .. versionadded:: 2015.5.0 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: if distro == "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 templates(): """ .. versionadded:: 2015.5.0 List the available LXC template scripts installed on the minion CLI Examples: .. code-block:: bash salt myminion lxc.templates """ try: template_scripts = os.listdir("/usr/share/lxc/templates") except OSError: return [] else: return [x[4:] for x in template_scripts if x.startswith("lxc-")] def _after_ignition_network_profile(cmd, ret, name, network_profile, path, nic_opts): _clear_context() if ret["retcode"] == 0 and exists(name, path=path): if network_profile: network_changes = apply_network_profile( name, network_profile, path=path, nic_opts=nic_opts ) if network_changes: log.info( "Network changes from applying network profile '%s' " "to newly-created container '%s':\n%s", network_profile, name, network_changes, ) c_state = state(name, path=path) return {"result": True, "state": {"old": None, "new": c_state}} else: if exists(name, path=path): # destroy the container if it was partially created cmd = "lxc-destroy" if path: cmd += f" -P {shlex.quote(path)}" cmd += f" -n {name}" __salt__["cmd.retcode"](cmd, python_shell=False) raise CommandExecutionError( "Container could not be created with cmd '{}': {}".format( cmd, ret["stderr"] ) ) def create( name, config=None, profile=None, network_profile=None, nic_opts=None, **kwargs ): """ Create a new container. 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 `). Values in a profile will be overridden by the **Container Creation Arguments** listed below. network_profile Network profile to use for container .. versionadded:: 2015.5.0 **Container Creation Arguments** template The template to use. For example, ``ubuntu`` or ``fedora``. For a full list of available templates, check out the :mod:`lxc.templates ` function. 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. 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"}' For available template options, refer to the lxc template scripts which are usually located under ``/usr/share/lxc/templates``, or run ``lxc-create -t