Source code for saltext.incus.states.incus

"""
States for managing Incus instances.

:py:func:`present`, :py:func:`running`, :py:func:`stopped` and :py:func:`absent`
manage instance lifecycle; :py:func:`sls_applied` applies SLS inside an instance
through the agentless thin path and surfaces the in-instance highstate result.

:py:func:`present` reconciles declared config and devices additively: it sets
what you declare and leaves undeclared keys, devices and profile membership
alone.

:depends: incus execution module
"""

from salt.exceptions import CommandExecutionError
from salt.exceptions import SaltInvocationError

__virtualname__ = "incus"


[docs] def __virtual__(): """Only load when the incus execution module is available.""" if "incus.info" in __salt__: return __virtualname__ return (False, "the incus execution module is not available")
def _stringify(value): if isinstance(value, bool): return "true" if value else "false" return str(value) def _normalize_device(device): return {key: _stringify(value) for key, value in (device or {}).items()} def _config_deltas(current_config, desired_config): """ Return ``{key: {"old": ..., "new": ...}}`` for declared keys that differ. """ deltas = {} for key, value in (desired_config or {}).items(): desired = _stringify(value) current = current_config.get(key) if current != desired: deltas[key] = {"old": current, "new": desired} return deltas def _device_deltas(current_devices, desired_devices): """ Return ``{device: {"old": ..., "new": ...}}`` for declared devices that are missing or differ. """ deltas = {} for device_name, device in (desired_devices or {}).items(): desired = _normalize_device(device) current = current_devices.get(device_name) current_norm = _normalize_device(current) if current is not None else None if current_norm != desired: deltas[device_name] = {"old": current, "new": device} return deltas
[docs] def present( name, image=None, profiles=None, config=None, devices=None, running=None, project=None, ): """ Ensure an instance exists with the declared config and devices. name Instance name. image Source image. Required only when the instance must be created. profiles Profiles to attach at creation. Not reconciled after creation. config Instance config keys to ensure are set. devices Devices to ensure are present, mapping device name to a definition that includes a ``type``. running ``True`` ensures the instance is running, ``False`` ensures it is stopped, ``None`` leaves the run state untouched. Example: .. code-block:: yaml web01: incus.present: - image: images:debian/12 - running: true - config: boot.autostart: true """ ret = {"name": name, "result": True, "changes": {}, "comment": ""} test = __opts__["test"] try: current = __salt__["incus.info"](name, project=project) except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not query instance '{name}': {exc}" return ret # --- create path ------------------------------------------------------- if current is None: if not image: ret["result"] = False ret["comment"] = "instance '{}' is absent and no image was given to create it".format( name ) return ret if test: ret["result"] = None ret["changes"] = {name: {"old": None, "new": f"created from {image}"}} ret["comment"] = f"instance '{name}' would be created from {image}" return ret try: __salt__["incus.create"]( name, image, profiles=profiles, config=config, devices=devices, start=(running is True), project=project, ) if running is False: # creation leaves it stopped already; nothing to do pass except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not create instance '{name}': {exc}" return ret ret["changes"] = {name: {"old": None, "new": f"created from {image}"}} ret["comment"] = f"instance '{name}' created" return ret # --- reconcile path ---------------------------------------------------- config_deltas = _config_deltas(current.get("config", {}), config) device_deltas = _device_deltas(current.get("devices", {}), devices) status = current.get("status") run_delta = None if running is True and status != "Running": run_delta = {"old": status, "new": "Running"} elif running is False and status == "Running": run_delta = {"old": status, "new": "Stopped"} if not config_deltas and not device_deltas and not run_delta: ret["comment"] = f"instance '{name}' is already in the declared state" return ret changes = {} if config_deltas: changes["config"] = config_deltas if device_deltas: changes["devices"] = device_deltas if run_delta: changes["status"] = run_delta if test: ret["result"] = None ret["changes"] = changes ret["comment"] = f"instance '{name}' would be updated" return ret try: for key, delta in config_deltas.items(): __salt__["incus.config_set"](name, key, delta["new"], project=project) for device_name, delta in device_deltas.items(): if delta["old"] is not None: __salt__["incus.device_remove"](name, device_name, project=project) device = dict(delta["new"]) device_type = device.pop("type", None) if device_type is None: raise CommandExecutionError(f"device '{device_name}' is missing a 'type'") __salt__["incus.device_add"](name, device_name, device_type, project=project, **device) if run_delta and run_delta["new"] == "Running": __salt__["incus.start"](name, project=project) elif run_delta and run_delta["new"] == "Stopped": __salt__["incus.stop"](name, project=project) except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not update instance '{name}': {exc}" ret["changes"] = changes return ret ret["changes"] = changes ret["comment"] = f"instance '{name}' updated" return ret
[docs] def running(name, project=None): """ Ensure an existing instance is running. Example: .. code-block:: yaml web01 running: incus.running: - name: web01 """ ret = {"name": name, "result": True, "changes": {}, "comment": ""} test = __opts__["test"] current = __salt__["incus.info"](name, project=project) if current is None: ret["result"] = False ret["comment"] = f"instance '{name}' does not exist" return ret if current.get("status") == "Running": ret["comment"] = f"instance '{name}' is already running" return ret if test: ret["result"] = None ret["changes"] = {"status": {"old": current.get("status"), "new": "Running"}} ret["comment"] = f"instance '{name}' would be started" return ret try: __salt__["incus.start"](name, project=project) except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not start instance '{name}': {exc}" return ret ret["changes"] = {"status": {"old": current.get("status"), "new": "Running"}} ret["comment"] = f"instance '{name}' started" return ret
[docs] def stopped(name, timeout=30, force=False, project=None): """ Ensure an existing instance is stopped. Example: .. code-block:: yaml web01 stopped: incus.stopped: - name: web01 """ ret = {"name": name, "result": True, "changes": {}, "comment": ""} test = __opts__["test"] current = __salt__["incus.info"](name, project=project) if current is None: ret["result"] = False ret["comment"] = f"instance '{name}' does not exist" return ret if current.get("status") != "Running": ret["comment"] = f"instance '{name}' is already stopped" return ret if test: ret["result"] = None ret["changes"] = {"status": {"old": current.get("status"), "new": "Stopped"}} ret["comment"] = f"instance '{name}' would be stopped" return ret try: __salt__["incus.stop"](name, timeout=timeout, force=force, project=project) except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not stop instance '{name}': {exc}" return ret ret["changes"] = {"status": {"old": current.get("status"), "new": "Stopped"}} ret["comment"] = f"instance '{name}' stopped" return ret
[docs] def absent(name, force=True, project=None): """ Ensure an instance does not exist. Example: .. code-block:: yaml web01 absent: incus.absent: - name: web01 """ ret = {"name": name, "result": True, "changes": {}, "comment": ""} test = __opts__["test"] current = __salt__["incus.info"](name, project=project) if current is None: ret["comment"] = f"instance '{name}' is already absent" return ret if test: ret["result"] = None ret["changes"] = {name: {"old": "present", "new": "absent"}} ret["comment"] = f"instance '{name}' would be deleted" return ret try: __salt__["incus.delete"](name, force=force, project=project) except CommandExecutionError as exc: ret["result"] = False ret["comment"] = f"could not delete instance '{name}': {exc}" return ret ret["changes"] = {name: {"old": "present", "new": "absent"}} ret["comment"] = f"instance '{name}' deleted" return ret
def _process_inner_highstate(ret, inner, test): """ Map the in-instance highstate result onto the outer state return. A failed inner state makes the outer state fail. In test mode the outer result is ``None``. Inner changes surface through the outer ``changes``. """ if not isinstance(inner, dict) or not inner: ret["result"] = False ret["comment"] = "in-instance apply returned no state data" if inner: ret["comment"] += f": {inner}" return ret failed = [] changed = {} total = 0 for state_id, sdata in inner.items(): if not isinstance(sdata, dict): continue total += 1 if sdata.get("result") is False: failed.append((state_id, sdata.get("comment", ""))) if sdata.get("changes"): changed[state_id] = sdata["changes"] if failed: ret["result"] = False elif test: ret["result"] = None else: ret["result"] = True if changed: ret["changes"] = changed if failed: ret["comment"] = "{} of {} in-instance states failed: {}".format( len(failed), total, "; ".join(f"{sid} ({msg})" for sid, msg in failed) ) elif test: ret["comment"] = "{} in-instance states would be applied ({} with changes)".format( total, len(changed) ) else: ret["comment"] = f"applied {total} in-instance states ({len(changed)} changed)" return ret
[docs] def sls_applied( name, mods, pillar=None, saltenv="base", transport="thin", pillar_mode="file", precompiled=False, cleanup=True, project=None, ): """ Apply SLS ``mods`` inside a running instance via the agentless thin path. The instance must already exist and be running (compose this after :py:func:`present` or :py:func:`running`). The in-instance highstate result is mapped onto this state's result: any failed inner state fails this state, ``test=True`` yields a ``None`` result, and the inner changes are reported through ``changes``. name Instance name. mods SLS modules to apply, as a list or comma-separated string. pillar Pillar to use, already resolved on the control node. precompiled : False Compile on the control node and ship low state instead of SLS source. See :py:func:`incus.sls <salt.modules.incus.sls>`. Example: .. code-block:: yaml web01 configured: incus.sls_applied: - name: web01 - mods: - access.users - access.sshd - pillar: access: {{ salt['pillar.get']('access') | json }} """ ret = {"name": name, "result": None, "changes": {}, "comment": ""} test = __opts__["test"] current = __salt__["incus.info"](name, project=project) if current is None: ret["result"] = False ret["comment"] = f"instance '{name}' does not exist" return ret if current.get("status") != "Running": ret["result"] = False ret["comment"] = f"instance '{name}' is not running" return ret try: inner = __salt__["incus.sls"]( name, mods, saltenv=saltenv, pillar=pillar, test=test, transport=transport, pillar_mode=pillar_mode, precompiled=precompiled, cleanup=cleanup, project=project, ) except (CommandExecutionError, SaltInvocationError) as exc: ret["result"] = False ret["comment"] = f"in-instance apply failed: {exc}" return ret return _process_inner_highstate(ret, inner, test)