File: //opt/saltstack/salt/lib/python3.10/site-packages/salt/runners/lxc.py
"""
Control Linux Containers via Salt
:depends: lxc execution module
"""
import copy
import logging
import os
import time
from collections import OrderedDict as _OrderedDict
import salt.client
import salt.key
import salt.utils.args
import salt.utils.cloud
import salt.utils.files
import salt.utils.stringutils
import salt.utils.virt
log = logging.getLogger(__name__)
# Don't shadow built-in's.
__func_alias__ = {"list_": "list"}
def _do(name, fun, path=None):
"""
Invoke a function in the lxc module with no args
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
"""
host = find_guest(name, quiet=True, path=path)
if not host:
return False
with salt.client.get_local_client(__opts__["conf_file"]) as client:
cmd_ret = client.cmd_iter(
host, f"lxc.{fun}", [name], kwarg={"path": path}, timeout=60
)
data = next(cmd_ret)
data = data.get(host, {}).get("ret", None)
if data:
data = {host: data}
return data
def _do_names(names, fun, path=None):
"""
Invoke a function in the lxc module with no args
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
"""
ret = {}
hosts = find_guests(names, path=path)
if not hosts:
return False
with salt.client.get_local_client(__opts__["conf_file"]) as client:
for host, sub_names in hosts.items():
cmds = []
for name in sub_names:
cmds.append(
client.cmd_iter(
host,
f"lxc.{fun}",
[name],
kwarg={"path": path},
timeout=60,
)
)
for cmd in cmds:
data = next(cmd)
data = data.get(host, {}).get("ret", None)
if data:
ret.update({host: data})
return ret
def find_guest(name, quiet=False, path=None):
"""
Returns the host for a container.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.find_guest name
"""
if quiet:
log.warning("'quiet' argument is being deprecated. Please migrate to --quiet")
for data in _list_iter(path=path):
host, l = next(iter(data.items()))
for x in "running", "frozen", "stopped":
if name in l[x]:
if not quiet:
__jid_event__.fire_event(
{"data": host, "outputter": "lxc_find_host"}, "progress"
)
return host
return None
def find_guests(names, path=None):
"""
Return a dict of hosts and named guests
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
"""
ret = {}
names = names.split(",")
for data in _list_iter(path=path):
host, stat = next(iter(data.items()))
for state in stat:
for name in stat[state]:
if name in names:
if host in ret:
ret[host].append(name)
else:
ret[host] = [name]
return ret
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] \\
[cpushare=cgroups_cpushare] [memory=cgroups_memory] \\
[template=lxc_template_name] [clone=original name] \\
[profile=lxc_profile] [network_proflile=network_profile] \\
[nic=network_profile] [nic_opts=nic_opts] \\
[start=(true|false)] [seed=(true|false)] \\
[install=(true|false)] [config=minion_config] \\
[snapshot=(true|false)]
names
Name of the containers, supports a single name or a comma delimited
list of names.
host
Minion on which to initialize the container **(required)**
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
saltcloud_mode
init the container with the saltcloud opts format instead
See lxc.init_interface module documentation
cpuset
cgroups cpuset.
cpushare
cgroups cpu shares.
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.
template
Name of LXC template on which to base this container
clone
Clone this container from an existing container
profile
A LXC profile (defined in config or pillar).
network_profile
Network profile to use for the container
.. versionadded:: 2015.5.2
nic
.. deprecated:: 2015.5.0
Use ``network_profile`` instead
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"}}``
start
Start the newly created container.
seed
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.
"""
path = kwargs.get("path", None)
if quiet:
log.warning("'quiet' argument is being deprecated. Please migrate to --quiet")
ret = {"comment": "", "result": True}
if host is None:
# 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, str):
names = names.split(",")
if not isinstance(names, list):
ret["comment"] = "Container names are not formed as a list"
ret["result"] = False
return ret
# check that the host is alive
alive = False
with salt.client.get_local_client(__opts__["conf_file"]) as client:
try:
if client.cmd(host, "test.ping", timeout=20).get(host, None):
alive = True
except (TypeError, KeyError):
pass
if not alive:
ret["comment"] = f"Host {host} is not reachable"
ret["result"] = False
return ret
log.info("Searching for LXC Hosts")
data = __salt__["lxc.list"](host, quiet=True, path=path)
for host, containers in data.items():
for name in names:
if name in sum(containers.values(), []):
log.info(
"Container '%s' already exists on host '%s', init "
"can be a NO-OP",
name,
host,
)
if host not in data:
ret["comment"] = f"Host '{host}' was not found"
ret["result"] = False
return ret
kw = salt.utils.args.clean_kwargs(**kwargs)
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 = {}
seed_arg = kwargs.get("seed", True)
if approve_key and not explicit_auth:
with salt.key.Key(__opts__) as skey:
all_minions = skey.all_keys().get("minions", [])
for name in names:
seed = seed_arg
if name in all_minions:
try:
if client.cmd(name, "test.ping", timeout=20).get(
name, None
):
seed = False
except (TypeError, KeyError):
pass
seeds[name] = seed
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) '%s' on host '%s'", names, host)
cmds = []
for name in names:
args = [name]
kw = salt.utils.args.clean_kwargs(**kwargs)
if saltcloud_mode:
kw = copy.deepcopy(kw)
kw["name"] = name
saved_kwargs = kw
kw = client.cmd(
host,
"lxc.cloud_init_interface",
args + [kw],
tgt_type="list",
timeout=600,
).get(host, {})
kw.update(saved_kwargs)
name = kw.pop("name", name)
# be sure not to seed an already seeded host
kw["seed"] = seeds.get(name, seed_arg)
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 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 = "Invalid return for {}: {} {}".format(
container_name, container, sub_ret
)
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 provisioned one container
ret["ping_status"] = bool(len(done))
# for all provisioned 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 salt.utils.files.fopen(key) as fic:
fcontent = salt.utils.stringutils.to_unicode(fic.read()).strip()
pub_key = salt.utils.stringutils.to_unicode(pub_key)
if pub_key.strip() != fcontent:
with salt.utils.files.fopen(key, "w") as fic:
fic.write(salt.utils.stringutils.to_str(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(f"Unresponsive {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:
__jid_event__.fire_event({"message": ret}, "progress")
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.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
saltcloud_mode
init the container with the saltcloud opts format instead
"""
if quiet:
log.warning("'quiet' argument is being deprecated. Please migrate to --quiet")
return __salt__["lxc.init"](
names=names, host=host, saltcloud_mode=True, quiet=quiet, **kwargs
)
def _list_iter(host=None, path=None):
"""
Return a generator iterating over hosts
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
"""
tgt = host or "*"
with salt.client.get_local_client(__opts__["conf_file"]) as client:
for container_info in client.cmd_iter(tgt, "lxc.list", kwarg={"path": path}):
if not container_info:
continue
if not isinstance(container_info, dict):
continue
chunk = {}
id_ = next(iter(container_info.keys()))
if host and host != id_:
continue
if not isinstance(container_info[id_], dict):
continue
if "ret" not in container_info[id_]:
continue
if not isinstance(container_info[id_]["ret"], dict):
continue
chunk[id_] = container_info[id_]["ret"]
yield chunk
def list_(host=None, quiet=False, path=None):
"""
List defined containers (running, stopped, and frozen) for the named
(or all) host(s).
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.list [host=minion_id]
"""
it = _list_iter(host, path=path)
ret = {}
for chunk in it:
ret.update(chunk)
if not quiet:
__jid_event__.fire_event(
{"data": chunk, "outputter": "lxc_list"}, "progress"
)
return ret
def purge(name, delete_key=True, quiet=False, path=None):
"""
Purge the named container and delete its minion key if present.
WARNING: Destroys all data associated with the container.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.purge name
"""
data = _do_names(name, "destroy", path=path)
if data is False:
return data
if delete_key:
with salt.key.Key(__opts__) as skey:
skey.delete_key(name)
if data is None:
return
if not quiet:
__jid_event__.fire_event({"data": data, "outputter": "lxc_purge"}, "progress")
return data
def start(name, quiet=False, path=None):
"""
Start the named container.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.start name
"""
data = _do_names(name, "start", path=path)
if data and not quiet:
__jid_event__.fire_event({"data": data, "outputter": "lxc_start"}, "progress")
return data
def stop(name, quiet=False, path=None):
"""
Stop the named container.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.stop name
"""
data = _do_names(name, "stop", path=path)
if data and not quiet:
__jid_event__.fire_event(
{"data": data, "outputter": "lxc_force_off"}, "progress"
)
return data
def freeze(name, quiet=False, path=None):
"""
Freeze the named container
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.freeze name
"""
data = _do_names(name, "freeze")
if data and not quiet:
__jid_event__.fire_event({"data": data, "outputter": "lxc_pause"}, "progress")
return data
def unfreeze(name, quiet=False, path=None):
"""
Unfreeze the named container
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.unfreeze name
"""
data = _do_names(name, "unfreeze", path=path)
if data and not quiet:
__jid_event__.fire_event({"data": data, "outputter": "lxc_resume"}, "progress")
return data
def info(name, quiet=False, path=None):
"""
Returns information about a container.
path
path to the container parent
default: /var/lib/lxc (system default)
.. versionadded:: 2015.8.0
.. code-block:: bash
salt-run lxc.info name
"""
data = _do_names(name, "info", path=path)
if data and not quiet:
__jid_event__.fire_event({"data": data, "outputter": "lxc_info"}, "progress")
return data