From 686529e73fb099af90ebce9a6c5e1f432c6484f9 Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Tue, 21 Jul 2020 11:00:10 +0200 Subject: [PATCH] Support transactional systems (MicroOS) Add rebootmgr module Add transactional_update module chroot: add chroot detector systemd: add offline mode detector transactional_update: add pending_transaction detector extra: add EFI and transactional grains transactional_update: add call, apply_, sls & highstate transactional_update: add documentation transactional_update: add executor Add changelog entry 58519.added Closes #58519 transactional_update: update the cleanups family transactional_update: add activate_transaction param transactional_update: skip tests on Windows transactional_update: unify with chroot.call Return for both .call() "retcode" when fail Add MicroOS information in release note systemd: support NamedLoaderContext transactional_update: detect recursion in the executor Handle master tops data when states are applied by transactional_update Fix unit tests for transactional_update module Do noop for services states when running systemd in offline mode transactional_updates: do not execute states in parallel but use a queue Add changes suggested by pre-commit Fix unit tests for transactional_updates module Add unit tests to cover queue cases on transaction_update states Refactor offline checkers and add unit tests Fix regression that always consider offline mode Add proper mocking and skip tests when running in offline mode Fix failing unit tests for systemd test_rebootmgr: convert to pytest test_transactional_update: convert to pytest Update release documentation to 3004 --- changelog/58519.added | 1 + doc/ref/executors/all/index.rst | 1 + .../salt.executors.transactional_update.rst | 6 + doc/ref/modules/all/index.rst | 2 + .../modules/all/salt.modules.rebootmgr.rst | 5 + .../all/salt.modules.transactional_update.rst | 5 + doc/topics/releases/3004.rst | 45 + salt/executors/transactional_update.py | 132 ++ salt/grains/extra.py | 37 + salt/modules/chroot.py | 75 +- salt/modules/rebootmgr.py | 359 +++++ salt/modules/systemd_service.py | 28 +- salt/modules/transactional_update.py | 1325 +++++++++++++++++ salt/states/service.py | 14 + salt/utils/systemd.py | 26 +- tests/integration/states/test_service.py | 4 + tests/pytests/unit/modules/test_rebootmgr.py | 309 ++++ .../unit/modules/test_transactional_update.py | 999 +++++++++++++ tests/pytests/unit/states/test_service.py | 34 + tests/unit/modules/test_chroot.py | 41 +- tests/unit/modules/test_systemd_service.py | 24 +- 21 files changed, 3441 insertions(+), 31 deletions(-) create mode 100644 changelog/58519.added create mode 100644 doc/ref/executors/all/salt.executors.transactional_update.rst create mode 100644 doc/ref/modules/all/salt.modules.rebootmgr.rst create mode 100644 doc/ref/modules/all/salt.modules.transactional_update.rst create mode 100644 salt/executors/transactional_update.py create mode 100644 salt/modules/rebootmgr.py create mode 100644 salt/modules/transactional_update.py create mode 100644 tests/pytests/unit/modules/test_rebootmgr.py create mode 100644 tests/pytests/unit/modules/test_transactional_update.py diff --git a/changelog/58519.added b/changelog/58519.added new file mode 100644 index 0000000000..1cc8d7dc74 --- /dev/null +++ b/changelog/58519.added @@ -0,0 +1 @@ +Add support for transactional systems, like openSUSE MicroOS \ No newline at end of file diff --git a/doc/ref/executors/all/index.rst b/doc/ref/executors/all/index.rst index 1f26a86fc3..4cd430d8e3 100644 --- a/doc/ref/executors/all/index.rst +++ b/doc/ref/executors/all/index.rst @@ -14,3 +14,4 @@ executors modules docker splay sudo + transactional_update diff --git a/doc/ref/executors/all/salt.executors.transactional_update.rst b/doc/ref/executors/all/salt.executors.transactional_update.rst new file mode 100644 index 0000000000..17f00b2d27 --- /dev/null +++ b/doc/ref/executors/all/salt.executors.transactional_update.rst @@ -0,0 +1,6 @@ +salt.executors.transactional_update module +========================================== + +.. automodule:: salt.executors.transactional_update + :members: + diff --git a/doc/ref/modules/all/index.rst b/doc/ref/modules/all/index.rst index 3fff7ad636..73958181dd 100644 --- a/doc/ref/modules/all/index.rst +++ b/doc/ref/modules/all/index.rst @@ -393,6 +393,7 @@ execution modules rbac_solaris rbenv rdp + rebootmgr redismod reg rest_pkg @@ -479,6 +480,7 @@ execution modules tls tomcat trafficserver + transactional_update travisci tuned twilio_notify diff --git a/doc/ref/modules/all/salt.modules.rebootmgr.rst b/doc/ref/modules/all/salt.modules.rebootmgr.rst new file mode 100644 index 0000000000..22240080b0 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.rebootmgr.rst @@ -0,0 +1,5 @@ +salt.modules.rebootmgr module +============================= + +.. automodule:: salt.modules.rebootmgr + :members: diff --git a/doc/ref/modules/all/salt.modules.transactional_update.rst b/doc/ref/modules/all/salt.modules.transactional_update.rst new file mode 100644 index 0000000000..2f15b95ad4 --- /dev/null +++ b/doc/ref/modules/all/salt.modules.transactional_update.rst @@ -0,0 +1,5 @@ +salt.modules.transactional_update module +======================================== + +.. automodule:: salt.modules.transactional_update + :members: diff --git a/doc/topics/releases/3004.rst b/doc/topics/releases/3004.rst index 4cb9527f6a..638e2f4aad 100644 --- a/doc/topics/releases/3004.rst +++ b/doc/topics/releases/3004.rst @@ -6,3 +6,48 @@ Salt 3004 Release Notes - Codename Silicon Salt 3004 is an *unreleased* upcoming feature release. +Changed +======= + +State Engine support for ``onfail`` as a requisite now can be +used with multiple requisites. This behavior was previously +broken and is described in detail at https://github.com/saltstack/salt/issues/59026 + + +New Features +============ + +Transactional System Support (MicroOS) +-------------------------------------- + +A transactional system, like ``MicroOS``, can present some challenges +when the user decided to manage it via Salt. + +MicroOS provide a read-only rootfs and a tool, +``transactional-update``, that takes care of the management of the +system (updating, upgrading, installation or reboot, among others) in +an atomic way. + +Atomicity is the main feature of MicroOS, and to guarantee this +property, this model leverages ``snapper``, ``zypper``, ``btrfs`` and +``overlayfs`` to create snapshots that will be updated independently +of the currently running system, and that are activated after the +reboot. This implies, for example, that some changes made on the +system are not visible until the next reboot, as those changes are +living in a different snapshot of the file system. + +Salt 3004 (Silicon) support this type of system via two new modules +(``transactional_update`` and ``rebootmgr``) and a new executor +(``transactional_update``). + +The new modules will provide all the low level API for interacting +with transactional systems, like defining a mantenance window where +the system is free to reboot and activate the new state, or install +new software in a new transaction. It will also provide hight level +of abstractions that will allows us to execute Salt module functions +or applying states inside new transactions. + +The execution module will help us to treat the transactional system +transparently (like the traditional ones), using a mechanism that will +delegate some Salt modules execution into the new +``transactional_update`` module. diff --git a/salt/executors/transactional_update.py b/salt/executors/transactional_update.py new file mode 100644 index 0000000000..6f36ed03b6 --- /dev/null +++ b/salt/executors/transactional_update.py @@ -0,0 +1,132 @@ +""" +Transactional executor module + +.. versionadded:: 3004 + +""" + +import os + +import salt.utils.path + +# Functions that are mapped into an equivalent one in +# transactional_update module +DELEGATION_MAP = { + "state.single": "transactional_update.single", + "state.sls": "transactional_update.sls", + "state.apply": "transactional_update.apply", + "state.highstate": "transactional_update.highstate", +} + +# By default, all modules and functions are executed outside the +# transaction. The next two sets will enumerate the exceptions that +# will be routed to transactional_update.call() +DEFAULT_DELEGATED_MODULES = [ + "ansible", + "cabal", + "chef", + "cmd", + "composer", + "cp", + "cpan", + "cyg", + "file", + "freeze", + "nix", + "npm", + "pip", + "pkg", + "puppet", + "pyenv", + "rbenv", + "scp", +] +DEFAULT_DELEGATED_FUNCTIONS = [] + + +def __virtual__(): + if salt.utils.path.which("transactional-update"): + return True + else: + return (False, "transactional_update executor requires a transactional system") + + +def execute(opts, data, func, args, kwargs): + """Delegate into transactional_update module + + The ``transactional_update`` module support the execution of + functions inside a transaction, as support apply a state (via + ``apply``, ``sls``, ``single`` or ``highstate``). + + This execution module can be used to route some Salt modules and + functions to be executed inside the transaction snapshot. + + Add this executor in the minion configuration file: + + .. code-block:: yaml + + module_executors: + - transactional_update + - direct_call + + Or use the command line parameter: + + .. code-block:: bash + + salt-call --module-executors='[transactional_update, direct_call]' test.version + + You can also schedule a reboot if needed: + + .. code-block:: bash + + salt-call --module-executors='[transactional_update]' state.sls stuff activate_transaction=True + + There are some configuration parameters supported: + + .. code-block:: yaml + + # Replace the list of default modules that all the functions + # are delegated to `transactional_update.call()` + delegated_modules: [cmd, pkg] + + # Replace the list of default functions that are delegated to + # `transactional_update.call()` + delegated_functions: [pip.install] + + # Expand the default list of modules + add_delegated_modules: [ansible] + + # Expand the default list of functions + add_delegated_functions: [file.copy] + + """ + inside_transaction = os.environ.get("TRANSACTIONAL_UPDATE") + + fun = data["fun"] + module, _ = fun.split(".") + + delegated_modules = set(opts.get("delegated_modules", DEFAULT_DELEGATED_MODULES)) + delegated_functions = set( + opts.get("delegated_functions", DEFAULT_DELEGATED_FUNCTIONS) + ) + if "executor_opts" in data: + delegated_modules |= set(data["executor_opts"].get("add_delegated_modules", [])) + delegated_functions |= set( + data["executor_opts"].get("add_delegated_functions", []) + ) + else: + delegated_modules |= set(opts.get("add_delegated_modules", [])) + delegated_functions |= set(opts.get("add_delegated_functions", [])) + + if fun in DELEGATION_MAP and not inside_transaction: + result = __executors__["direct_call.execute"]( + opts, data, __salt__[DELEGATION_MAP[fun]], args, kwargs + ) + elif ( + module in delegated_modules or fun in delegated_functions + ) and not inside_transaction: + result = __salt__["transactional_update.call"](fun, *args, **kwargs) + else: + result = __executors__["direct_call.execute"](opts, data, func, args, kwargs) + + return result diff --git a/salt/grains/extra.py b/salt/grains/extra.py index 0eec27e628..9fd7b2b8b1 100644 --- a/salt/grains/extra.py +++ b/salt/grains/extra.py @@ -3,14 +3,17 @@ from __future__ import absolute_import, print_function, unicode_literals # Import third party libs +import glob import logging # Import python libs import os # Import salt libs +import salt.utils import salt.utils.data import salt.utils.files +import salt.utils.path import salt.utils.platform import salt.utils.yaml @@ -74,3 +77,37 @@ def suse_backported_capabilities(): '__suse_reserved_pkg_patches_support': True, '__suse_reserved_saltutil_states_support': True } + +def __secure_boot(efivars_dir): + """Detect if secure-boot is enabled.""" + enabled = False + sboot = glob.glob(os.path.join(efivars_dir, "SecureBoot-*/data")) + if len(sboot) == 1: + # The minion is usually running as a privileged user, but is + # not the case for the master. Seems that the master can also + # pick the grains, and this file can only be readed by "root" + try: + with salt.utils.files.fopen(sboot[0], "rb") as fd: + enabled = fd.read()[-1:] == b"\x01" + except PermissionError: + pass + return enabled + + +def uefi(): + """Populate UEFI grains.""" + efivars_dir = next( + filter(os.path.exists, ["/sys/firmware/efi/efivars", "/sys/firmware/efi/vars"]), + None, + ) + grains = { + "efi": bool(efivars_dir), + "efi-secure-boot": __secure_boot(efivars_dir) if efivars_dir else False, + } + + return grains + + +def transactional(): + """Determine if the system is transactional.""" + return {"transactional": bool(salt.utils.path.which("transactional-update"))} diff --git a/salt/modules/chroot.py b/salt/modules/chroot.py index 6512a70f88..6e917b5cf8 100644 --- a/salt/modules/chroot.py +++ b/salt/modules/chroot.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- - """ :maintainer: Alberto Planas :maturity: new :depends: None :platform: Linux """ -from __future__ import absolute_import, print_function, unicode_literals import copy import logging @@ -19,8 +16,8 @@ import salt.client.ssh.state import salt.client.ssh.wrapper.state import salt.defaults.exitcodes import salt.exceptions -import salt.ext.six as six import salt.utils.args +import salt.utils.files __func_alias__ = {"apply_": "apply"} @@ -40,6 +37,16 @@ def __virtual__(): def exist(root): """ Return True if the chroot environment is present. + + root + Path to the chroot environment + + CLI Example: + + .. code-block:: bash + + salt myminion chroot.exist /chroot + """ dev = os.path.join(root, "dev") proc = os.path.join(root, "proc") @@ -79,6 +86,38 @@ def create(root): return True +def in_chroot(): + """ + Return True if the process is inside a chroot jail + + .. versionadded:: 3004 + + CLI Example: + + .. code-block:: bash + + salt myminion chroot.in_chroot + + """ + result = False + + try: + # We cannot assume that we are "root", so we cannot read + # '/proc/1/root', that is required for the usual way of + # detecting that we are in a chroot jail. We use the debian + # ischroot method. + with salt.utils.files.fopen( + "/proc/1/mountinfo" + ) as root_fd, salt.utils.files.fopen("/proc/self/mountinfo") as self_fd: + root_mountinfo = root_fd.read() + self_mountinfo = self_fd.read() + result = root_mountinfo != self_mountinfo + except OSError: + pass + + return result + + def call(root, function, *args, **kwargs): """ Executes a Salt function inside a chroot environment. @@ -116,7 +155,7 @@ def call(root, function, *args, **kwargs): so_mods=__salt__["config.option"]("thin_so_mods", ""), ) # Some bug in Salt is preventing us to use `archive.tar` here. A - # AsyncZeroMQReqChannel is not closed at the end os the salt-call, + # AsyncZeroMQReqChannel is not closed at the end of the salt-call, # and makes the client never exit. # # stdout = __salt__['archive.tar']('xzf', thin_path, dest=thin_dest_path) @@ -158,8 +197,12 @@ def call(root, function, *args, **kwargs): if isinstance(local, dict) and "retcode" in local: __context__["retcode"] = local["retcode"] return local.get("return", data) - except (KeyError, ValueError): - return {"result": False, "comment": "Can't parse container command output"} + except ValueError: + return { + "result": False, + "retcode": ret["retcode"], + "comment": {"stdout": ret["stdout"], "stderr": ret["stderr"]}, + } finally: __utils__["files.rm_rf"](thin_dest_path) @@ -194,19 +237,23 @@ def apply_(root, mods=None, **kwargs): def _create_and_execute_salt_state(root, chunks, file_refs, test, hash_type): """ - Create the salt_stage tarball, and execute in the chroot + Create the salt_state tarball, and execute in the chroot """ # Create the tar containing the state pkg and relevant files. salt.client.ssh.wrapper.state._cleanup_slsmod_low_data(chunks) trans_tar = salt.client.ssh.state.prep_trans_tar( - salt.fileclient.get_file_client(__opts__), chunks, file_refs, __pillar__, root + salt.fileclient.get_file_client(__opts__), + chunks, + file_refs, + __pillar__.value(), + root, ) trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, hash_type) ret = None # Create a temporary directory inside the chroot where we can move - # the salt_stage.tgz + # the salt_state.tgz salt_state_path = tempfile.mkdtemp(dir=root) salt_state_path = os.path.join(salt_state_path, "salt_state.tgz") salt_state_path_in_chroot = salt_state_path.replace(root, "", 1) @@ -260,7 +307,7 @@ def sls(root, mods, saltenv="base", test=None, exclude=None, **kwargs): """ # Get a copy of the pillar data, to avoid overwriting the current # pillar, instead the one delegated - pillar = copy.deepcopy(__pillar__) + pillar = copy.deepcopy(__pillar__.value()) pillar.update(kwargs.get("pillar", {})) # Clone the options data and apply some default values. May not be @@ -270,12 +317,12 @@ def sls(root, mods, saltenv="base", test=None, exclude=None, **kwargs): opts, pillar, __salt__, salt.fileclient.get_file_client(__opts__) ) - if isinstance(mods, six.string_types): + if isinstance(mods, str): mods = mods.split(",") high_data, errors = st_.render_highstate({saltenv: mods}) if exclude: - if isinstance(exclude, six.string_types): + if isinstance(exclude, str): exclude = exclude.split(",") if "__exclude__" in high_data: high_data["__exclude__"].extend(exclude) @@ -329,7 +376,7 @@ def highstate(root, **kwargs): """ # Get a copy of the pillar data, to avoid overwriting the current # pillar, instead the one delegated - pillar = copy.deepcopy(__pillar__) + pillar = copy.deepcopy(__pillar__.value()) pillar.update(kwargs.get("pillar", {})) # Clone the options data and apply some default values. May not be diff --git a/salt/modules/rebootmgr.py b/salt/modules/rebootmgr.py new file mode 100644 index 0000000000..4354dd1fce --- /dev/null +++ b/salt/modules/rebootmgr.py @@ -0,0 +1,359 @@ +""" +:maintainer: Alberto Planas +:maturity: new +:depends: None +:platform: Linux + +.. versionadded:: 3004 +""" + +import logging +import re + +import salt.exceptions + +log = logging.getLogger(__name__) + + +def __virtual__(): + """rebootmgrctl command is required.""" + if __utils__["path.which"]("rebootmgrctl") is not None: + return True + else: + return (False, "Module rebootmgt requires the command rebootmgrctl") + + +def _cmd(cmd, retcode=False): + """Utility function to run commands.""" + result = __salt__["cmd.run_all"](cmd) + if retcode: + return result["retcode"] + + if result["retcode"]: + raise salt.exceptions.CommandExecutionError(result["stderr"]) + + return result["stdout"] + + +def version(): + """Return the version of rebootmgrd + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr version + + """ + cmd = ["rebootmgrctl", "--version"] + + return _cmd(cmd).split()[-1] + + +def is_active(): + """Check if the rebootmgrd is running and active or not. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr is_active + + """ + cmd = ["rebootmgrctl", "is_active", "--quiet"] + + return _cmd(cmd, retcode=True) == 0 + + +def reboot(order=None): + """Tells rebootmgr to schedule a reboot. + + With the [now] option, a forced reboot is done, no lock from etcd + is requested and a set maintenance window is ignored. With the + [fast] option, a lock from etcd is requested if needed, but a + defined maintenance window is ignored. + + order + If specified, can be "now" or "fast" + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr reboot + salt microos rebootmgt reboot order=now + + """ + if order and order not in ("now", "fast"): + raise salt.exceptions.CommandExecutionError( + "Order parameter, if specified, must be 'now' or 'fast'" + ) + + cmd = ["rebootmgrctl", "reboot"] + if order: + cmd.append(order) + + return _cmd(cmd) + + +def cancel(): + """Cancels an already running reboot. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr cancel + + """ + cmd = ["rebootmgrctl", "cancel"] + + return _cmd(cmd) + + +def status(): + """Returns the current status of rebootmgrd. + + Valid returned values are: + 0 - No reboot requested + 1 - Reboot requested + 2 - Reboot requested, waiting for maintenance window + 3 - Reboot requested, waiting for etcd lock. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr status + + """ + cmd = ["rebootmgrctl", "status", "--quiet"] + + return _cmd(cmd, retcode=True) + + +def set_strategy(strategy=None): + """A new strategy to reboot the machine is set and written into + /etc/rebootmgr.conf. + + strategy + If specified, must be one of those options: + + best-effort - This is the default strategy. If etcd is + running, etcd-lock is used. If no etcd is running, but a + maintenance window is specified, the strategy will be + maint-window. If no maintenance window is specified, the + machine is immediately rebooted (instantly). + + etcd-lock - A lock at etcd for the specified lock-group will + be acquired before reboot. If a maintenance window is + specified, the lock is only acquired during this window. + + maint-window - Reboot does happen only during a specified + maintenance window. If no window is specified, the + instantly strategy is followed. + + instantly - Other services will be informed that a reboot will + happen. Reboot will be done without getting any locks or + waiting for a maintenance window. + + off - Reboot requests are temporary + ignored. /etc/rebootmgr.conf is not modified. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr set_strategy stragegy=off + + """ + if strategy and strategy not in ( + "best-effort", + "etcd-lock", + "maint-window", + "instantly", + "off", + ): + raise salt.exceptions.CommandExecutionError("Strategy parameter not valid") + + cmd = ["rebootmgrctl", "set-strategy"] + if strategy: + cmd.append(strategy) + + return _cmd(cmd) + + +def get_strategy(): + """The currently used reboot strategy of rebootmgrd will be printed. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr get_strategy + + """ + cmd = ["rebootmgrctl", "get-strategy"] + + return _cmd(cmd).split(":")[-1].strip() + + +def set_window(time, duration): + """Set's the maintenance window. + + time + The format of time is the same as described in + systemd.time(7). + + duration + The format of duration is "[XXh][YYm]". + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr set_window time="Thu,Fri 2020-*-1,5 11:12:13" duration=1h + + """ + cmd = ["rebootmgrctl", "set-window", time, duration] + + return _cmd(cmd) + + +def get_window(): + """The currently set maintenance window will be printed. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr get_window + + """ + cmd = ["rebootmgrctl", "get-window"] + window = _cmd(cmd) + + return dict( + zip( + ("time", "duration"), + re.search( + r"Maintenance window is set to (.*), lasting (.*).", window + ).groups(), + ) + ) + + +def set_group(group): + """Set the group, to which this machine belongs to get a reboot lock + from etcd. + + group + Group name + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr set_group group=group_1 + + """ + cmd = ["rebootmgrctl", "set-group", group] + + return _cmd(cmd) + + +def get_group(): + """The currently set lock group for etcd. + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr get_group + + """ + cmd = ["rebootmgrctl", "get-group"] + group = _cmd(cmd) + + return re.search(r"Etcd lock group is set to (.*)", group).groups()[0] + + +def set_max(max_locks, group=None): + """Set the maximal number of hosts in a group, which are allowed to + reboot at the same time. + + number + Maximal number of hosts in a group + + group + Group name + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr set_max 4 + + """ + cmd = ["rebootmgrctl", "set-max"] + if group: + cmd.extend(["--group", group]) + cmd.append(max_locks) + + return _cmd(cmd) + + +def lock(machine_id=None, group=None): + """Lock a machine. If no group is specified, the local default group + will be used. If no machine-id is specified, the local machine + will be locked. + + machine_id + The machine-id is a network wide, unique ID. Per default the + ID from /etc/machine-id is used. + + group + Group name + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr lock group=group1 + + """ + cmd = ["rebootmgrctl", "lock"] + if group: + cmd.extend(["--group", group]) + if machine_id: + cmd.append(machine_id) + + return _cmd(cmd) + + +def unlock(machine_id=None, group=None): + """Unlock a machine. If no group is specified, the local default group + will be used. If no machine-id is specified, the local machine + will be locked. + + machine_id + The machine-id is a network wide, unique ID. Per default the + ID from /etc/machine-id is used. + + group + Group name + + CLI Example: + + .. code-block:: bash + + salt microos rebootmgr unlock group=group1 + + """ + cmd = ["rebootmgrctl", "unlock"] + if group: + cmd.extend(["--group", group]) + if machine_id: + cmd.append(machine_id) + + return _cmd(cmd) diff --git a/salt/modules/systemd_service.py b/salt/modules/systemd_service.py index edc810db9b..df20de29a1 100644 --- a/salt/modules/systemd_service.py +++ b/salt/modules/systemd_service.py @@ -63,7 +63,10 @@ def __virtual__(): """ Only work on systems that have been booted with systemd """ - if __grains__.get("kernel") == "Linux" and salt.utils.systemd.booted(__context__): + is_linux = __grains__.get("kernel") == "Linux" + is_booted = salt.utils.systemd.booted(__context__) + is_offline = salt.utils.systemd.offline(__context__) + if is_linux and (is_booted or is_offline): return __virtualname__ return ( False, @@ -98,6 +101,11 @@ def _check_available(name): """ Returns boolean telling whether or not the named service is available """ + if offline(): + raise CommandExecutionError( + "Cannot run in offline mode. Failed to get information on unit '%s'" % name + ) + _status = _systemctl_status(name) sd_version = salt.utils.systemd.version(__context__) if sd_version is not None and sd_version >= 231: @@ -1452,3 +1460,21 @@ def firstboot( raise CommandExecutionError("systemd-firstboot error: {}".format(out["stderr"])) return True + + +def offline(): + """ + .. versionadded:: 3004 + + Check if systemd is working in offline mode, where is not possible + to talk with PID 1. + + CLI Example: + + .. code-block:: bash + + salt '*' service.offline + + """ + + return salt.utils.systemd.offline(__context__) diff --git a/salt/modules/transactional_update.py b/salt/modules/transactional_update.py new file mode 100644 index 0000000000..799fe08e4d --- /dev/null +++ b/salt/modules/transactional_update.py @@ -0,0 +1,1325 @@ +"""Transactional update +==================== + +.. versionadded:: 3004 + +A transactional system, like `MicroOS`_, can present some challenges +when the user decided to manage it via Salt. + +MicroOS provide a read-only rootfs and a tool, +``transactional-update``, that takes care of the management of the +system (updating, upgrading, installation or reboot, among others) in +an atomic way. + +Atomicity is the main feature of MicroOS, and to guarantee this +property, this model leverages ``snapper``, ``zypper``, ``btrfs`` and +``overlayfs`` to create snapshots that will be updated independently +of the currently running system, and that are activated after the +reboot. This implies, for example, that some changes made on the +system are not visible until the next reboot, as those changes are +living in a different snapshot of the file system. + +This model presents a lot of problems with the traditional Salt model, +where the inspections (like 'is this package installed?') are executed +in order to determine if a subsequent action is required (like +'install this package'). + +Lets consider this use case, to see how it works on a traditional +system, and in a transactional system: + +1) Check if ``apache`` is installed + +2) If it is not installed, install it + +3) Check that a ``vhost`` is configured for ``apache`` + +4) Make sure that ``apache2.service`` is enabled + +5) If the configuration changes, restart ``apache2.service`` + +In the traditional system everything will work as expected. The +system can see if the package is present or not, install it if it +isn't, and a re-check will shows that is already present. The same +will happen to the configuration file in ``/etc/apache2``, that will +be available as soon the package gets installed. Salt can inspect the +current form of this file, and add the missing bits if required. Salt +can annotate that a change is present, and restart the service. + +In a transactional system we will have multiple issues. The first one +is that Salt can only see the content of the snapshot where the system +booted from. Later snapshots may contain different content, including +the presence of ``apache``. If Salt decides to install ``apache`` +calling ``zypper``, it will fail, as this will try to write into the +read-only rootfs. Even if Salt would call ``transactional-update pkg +install``, the package would only be present in the new transaction +(snapshot), and will not be found in the currently running system when +later Salt tries to validate the presence of the package in the +current one. + +Any change in ``/etc`` alone will have also problems, as the changes +will be alive in a different overlay, only visible after the reboot. +And, finally, the service can only be enabled and restarted if the +service file is already present in the current ``/etc``. + + +General strategy +---------------- + +``transactional-update`` is the reference tool used for the +administration of transactional systems. Newer versions of this tool +support the execution of random commands in the new transaction, the +continuation of a transaction, the automatic detection of changes in +new transactions and the merge of ``/etc`` overlays. + +Continue a transaction +...................... + +One prerequisite already present is the support for branching from a +different snapshot than the current one in snapper. + +With this feature we can represent in ``transactional-update`` the +action of creating a transaction snapshot based on one that is planned +to be the active one after the reboot. This feature removes a lot of +user complains (like, for example, losing changes that are stored in a +transaction not yet activated), but also provide a more simple model +to work with. + +So, for example, if the user have this scenario:: + + +-----+ *=====* +--V--+ + --| T.1 |--| T.2 |--| T.3 | + +-----+ *=====* +--A--+ + +where T.2 is the current active one, and T.3 is an snapshot generated +from T.2 with a new package (``apache2``), and is marked to be the +active after the reboot. + +Previously, if the user (that is still on T.2) created a new +transaction, maybe for adding a new package (``tomcat``, for example), +the new T.4 will be based on the content of T.2 again, and not T.3, so +the new T.4 will have lost the changes of T.3 (i.e. `apache2` will not +be present in T.4). + +With the ``--continue`` parameter, ``transactional-update`` will +create T.4 based on T.3, and nothing will be lost. + +Command execution inside a new transaction +.......................................... + +With ``transactional-update run`` we will create a new transaction +based on the current one (T.2), where we can send interactive commands +that can modify the new transaction, and as commented, with +``transactional-update --continue run``, we will create a new +transaction based on the last created (T.3) + +The ``run`` command can execute any application inside the new +transaction namespace. This module uses this feature to execute the +different Salt execution modules, via ``call()``. Or even the full +``salt-thin`` or ``salt-call`` via ``sls()``, ``apply()``, +``single()`` or ``highstate``. + +``transactional-update`` will drop empty snapshots +.................................................. + +The option ``--drop-if-no-change`` is used to detect whether there is +any change in the file system on the read-only subvolume of the new +transaction will be added. If a change is present, the new +transaction will remain, if not it will be discarded. + +For example:: + + transactional-update --continue --drop-if-no-change run zypper in apache2" + +If we are in the scenario described before, ``apache2`` is already +present in T.3. In this case a new transaction, T.4, will be created +based on T.3, ``zypper`` will detect that the package is already +present and no change will be produced on T.4. At the end of the +execution, ``transactional-update`` will validate that T.3 and T.4 are +equivalent and T.4 will be discarded. + +If the command is:: + + transactional-update --continue --drop-if-no-change run zypper in tomcat + +the new T.4 will be indeed different from T.3, and will remain after +the transaction is closed. + +With this feature, every time that we call any function of this +execution module, we will minimize the amount of transaction, while +maintaining the idempotence so some operations. + +Report for pending transaction +.............................. + +A change in the system will create a new transaction, that needs to be +activated via a reboot. With ``pending_transaction()`` we can check +if a reboot is needed. We can execute the reboot using the +``reboot()`` function, that will follow the plan established by the +functions of the ``rebootmgr`` execution module. + +``/etc`` overlay merge when no new transaction is created +......................................................... + +In a transactional model, ``/etc`` is an overlay file system. Changes +done during the update are only present in the new transaction, and so +will only be available after the reboot. Or worse, if the transaction +gets dropped, because there is no change in the ``rootfs``, the +changes in ``/etc`` will be dropped too!. This is designed like that +in order to make the configuration files for the new package available +only when new package is also available to the user. So, after the +reboot. + +This makes sense for the case when, for example, ``apache2`` is not +present in the current transaction, but we installed it. The new +snapshot contains the ``apache2`` service, and the configuration files +in ``/etc`` will be accessible only after the reboot. + +But this model presents an issue. If we use ``transactional-update +--continue --drop-if-no-change run ``, where ```` +does not make any change in the read-only subvolume, but only in +``/etc`` (which is also read-write in the running system), the new +overlay with the changes in ``/etc`` will be dropped together with the +transaction. + +To fix this, ``transactional-update`` will detect that when no change +has been made on the read-only subvolume, but done in the overlay, the +transaction will be dropped and the changes in the overlay will be +merged back into ``/etc`` overlay of the current transaction. + + +Using the execution module +-------------------------- + +With this module we can create states that leverage Salt into this +kind of systems:: + + # Install apache (low-level API) + salt-call transactional_update.pkg_install apache2 + + # We can call any execution module + salt-call transactional_update.call pkg.install apache2 + + # Or via a state + salt-call transactional_update.single pkg.installed name=apache2 + + # We can also execute a zypper directly + salt-call transactional_update run "zypper in apache2" snapshot="continue" + + # We can reuse SLS states + salt-call transactional_update.apply install_and_configure_apache + + # Or apply the full highstate + salt-call transactional_update.highstate + + # Is there any change done in the system? + salt-call transactional_update pending_transaction + + # If so, reboot via rebootmgr + salt-call transactional_update reboot + + # We can enable the service + salt-call service.enable apache2 + + # If apache2 is available, this will work too + salt-call service.restart apache2 + + +Fixing some expectations +------------------------ + +This module alone is an improvement over the current state, but is +easy to see some limitations and problems: + +Is not a fully transparent approach +................................... + +The user needs to know if the system is transactional or not, as not +everything can be expressed inside a transaction (for example, +restarting a service inside transaction is not allowed). + +Two step for service restart +............................ + +In the ``apache2` example from the beginning we can observe the +biggest drawback. If the package ``apache2`` is missing, the new +module will create a new transaction, will execute ``pkg.install`` +inside the transaction (creating the salt-thin, moving it inside and +delegating the execution to `transactional-update` CLI as part of the +full state). Inside the transaction we can do too the required +changes in ``/etc`` for adding the new ``vhost``, and we can enable the +service via systemctl inside the same transaction. + +At this point we will not merge the ``/etc`` overlay into the current +one, and we expect from the user call the ``reboot`` function inside +this module, in order to activate the new transaction and start the +``apache2`` service. + +In the case that the package is already there, but the configuration +for the ``vhost`` is required, the new transaction will be dropped and +the ``/etc`` overlay will be visible in the live system. Then from +outside the transaction, via a different call to Salt, we can command +a restart of the ``apache2`` service. + +We can see that in both cases we break the user expectation, where a +change on the configuration will trigger automatically the restart of +the associated service. In a transactional scenario we need two +different steps: or a reboot, or a restart from outside of the +transaction. + +.. _MicroOS: https://microos.opensuse.org/ + +:maintainer: Alberto Planas +:maturity: new +:depends: None +:platform: Linux + +""" + +import copy +import logging +import os +import sys +import tempfile + +# required by _check_queue invocation later +import time # pylint: disable=unused-import + +import salt.client.ssh.state +import salt.client.ssh.wrapper.state +import salt.exceptions +import salt.utils.args +from salt.modules.state import _check_queue, _prior_running_states, _wait, running + +__func_alias__ = {"apply_": "apply"} + +log = logging.getLogger(__name__) + + +def __virtual__(): + """ + transactional-update command is required. + """ + global _check_queue, _wait, _prior_running_states, running + if __utils__["path.which"]("transactional-update"): + _check_queue = salt.utils.functools.namespaced_function(_check_queue, globals()) + _wait = salt.utils.functools.namespaced_function(_wait, globals()) + _prior_running_states = salt.utils.functools.namespaced_function( + _prior_running_states, globals() + ) + running = salt.utils.functools.namespaced_function(running, globals()) + return True + else: + return (False, "Module transactional_update requires a transactional system") + + +class TransactionalUpdateHighstate(salt.client.ssh.state.SSHHighState): + def _master_tops(self): + return self.client.master_tops() + + +def _global_params(self_update, snapshot=None, quiet=False): + """Utility function to prepare common global parameters.""" + params = ["--non-interactive", "--drop-if-no-change"] + if self_update is False: + params.append("--no-selfupdate") + if snapshot and snapshot != "continue": + params.extend(["--continue", snapshot]) + elif snapshot: + params.append("--continue") + if quiet: + params.append("--quiet") + return params + + +def _pkg_params(pkg, pkgs, args): + """Utility function to prepare common package parameters.""" + params = [] + + if not pkg and not pkgs: + raise salt.exceptions.CommandExecutionError("Provide pkg or pkgs parameters") + + if args and isinstance(args, str): + params.extend(args.split()) + elif args and isinstance(args, list): + params.extend(args) + + if pkg: + params.append(pkg) + + if pkgs and isinstance(pkgs, str): + params.extend(pkgs.split()) + elif pkgs and isinstance(pkgs, list): + params.extend(pkgs) + + return params + + +def _cmd(cmd, retcode=False): + """Utility function to run commands.""" + result = __salt__["cmd.run_all"](cmd) + if retcode: + return result["retcode"] + + if result["retcode"]: + raise salt.exceptions.CommandExecutionError(result["stderr"]) + + return result["stdout"] + + +def transactional(): + """Check if the system is a transactional system + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update transactional + + """ + return bool(__utils__["path.which"]("transactional-update")) + + +def in_transaction(): + """Check if Salt is executing while in a transaction + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update in_transaction + + """ + return transactional() and __salt__["chroot.in_chroot"]() + + +def cleanup(self_update=False): + """Run both cleanup-snapshots and cleanup-overlays. + + Identical to calling both cleanup-snapshots and cleanup-overlays. + + self_update + Check for newer transactional-update versions. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update cleanup + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update)) + cmd.append("cleanup") + return _cmd(cmd) + + +def cleanup_snapshots(self_update=False): + """Mark unused snapshots for snapper removal. + + If the current root filesystem is identical to the active root + filesystem (means after a reboot, before transactional-update + creates a new snapshot with updates), all old snapshots without a + cleanup algorithm get a cleanup algorithm set. This is to make + sure, that old snapshots will be deleted by snapper. See the + section about cleanup algorithms in snapper(8). + + self_update + Check for newer transactional-update versions. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update cleanup_snapshots + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update)) + cmd.append("cleanup-snapshots") + return _cmd(cmd) + + +def cleanup_overlays(self_update=False): + """Remove unused overlay layers. + + Removes all unreferenced (and thus unused) /etc overlay + directories in /var/lib/overlay. + + self_update + Check for newer transactional-update versions. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update cleanup_overlays + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update)) + cmd.append("cleanup-overlays") + return _cmd(cmd) + + +def grub_cfg(self_update=False, snapshot=None): + """Regenerate grub.cfg + + grub2-mkconfig(8) is called to create a new /boot/grub2/grub.cfg + configuration file for the bootloader. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update grub_cfg snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("grub.cfg") + return _cmd(cmd) + + +def bootloader(self_update=False, snapshot=None): + """Reinstall the bootloader + + Same as grub.cfg, but will also rewrite the bootloader itself. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update bootloader snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("bootloader") + return _cmd(cmd) + + +def initrd(self_update=False, snapshot=None): + """Regenerate initrd + + A new initrd is created in a snapshot. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update initrd snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("initrd") + return _cmd(cmd) + + +def kdump(self_update=False, snapshot=None): + """Regenerate kdump initrd + + A new initrd for kdump is created in a snapshot. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update kdump snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("kdump") + return _cmd(cmd) + + +def run(command, self_update=False, snapshot=None): + """Run a command in a new snapshot + + Execute the command inside a new snapshot. By default this snaphot + will remain, but if --drop-if-no-chage is set, the new snapshot + will be dropped if there is no change in the file system. + + command + Command with parameters that will be executed (as string or + array) + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update run "mkdir /tmp/dir" snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot, quiet=True)) + cmd.append("run") + if isinstance(command, str): + cmd.extend(command.split()) + elif isinstance(command, list): + cmd.extend(command) + else: + raise salt.exceptions.CommandExecutionError("Command parameter not recognized") + return _cmd(cmd) + + +def reboot(self_update=False): + """Reboot after update + + Trigger a reboot after updating the system. + + Several different reboot methods are supported, configurable via + the REBOOT_METHOD configuration option in + transactional-update.conf(5). By default rebootmgrd(8) will be + used to reboot the system according to the configured policies if + the service is running, otherwise systemctl reboot will be called. + + self_update + Check for newer transactional-update versions. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update reboot + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update)) + cmd.append("reboot") + return _cmd(cmd) + + +def dup(self_update=False, snapshot=None): + """Call 'zypper dup' + + If new updates are available, a new snapshot is created and zypper + dup --no-allow-vendor-change is used to update the + snapshot. Afterwards, the snapshot is activated and will be used + as the new root filesystem during next boot. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update dup snapshot="continue" + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("dup") + return _cmd(cmd) + + +def up(self_update=False, snapshot=None): + """Call 'zypper up' + + If new updates are available, a new snapshot is created and zypper + up is used to update the snapshot. Afterwards, the snapshot is + activated and will be used as the new root filesystem during next + boot. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update up snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("up") + return _cmd(cmd) + + +def patch(self_update=False, snapshot=None): + """Call 'zypper patch' + + If new updates are available, a new snapshot is created and zypper + patch is used to update the snapshot. Afterwards, the snapshot is + activated and will be used as the new root filesystem during next + boot. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update patch snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("patch") + return _cmd(cmd) + + +def migration(self_update=False, snapshot=None): + """Updates systems registered via SCC / SMT + + On systems which are registered against the SUSE Customer Center + (SCC) or SMT, a migration to a new version of the installed + products can be made with this option. + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update migration snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.append("migration") + return _cmd(cmd) + + +def pkg_install(pkg=None, pkgs=None, args=None, self_update=False, snapshot=None): + """Install individual packages + + Installs additional software. See the install description in the + "Package Management Commands" section of zypper's man page for all + available arguments. + + pkg + Package name to install + + pkgs + List of packages names to install + + args + String or list of extra parameters for zypper + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update pkg_install pkg=emacs snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.extend(["pkg", "install"]) + cmd.extend(_pkg_params(pkg, pkgs, args)) + return _cmd(cmd) + + +def pkg_remove(pkg=None, pkgs=None, args=None, self_update=False, snapshot=None): + """Remove individual packages + + Removes installed software. See the remove description in the + "Package Management Commands" section of zypper's man page for all + available arguments. + + pkg + Package name to install + + pkgs + List of packages names to install + + args + String or list of extra parameters for zypper + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update pkg_remove pkg=vim snapshot="continue" + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.extend(["pkg", "remove"]) + cmd.extend(_pkg_params(pkg, pkgs, args)) + return _cmd(cmd) + + +def pkg_update(pkg=None, pkgs=None, args=None, self_update=False, snapshot=None): + """Updates individual packages + + Update selected software. See the update description in the + "Update Management Commands" section of zypper's man page for all + available arguments. + + pkg + Package name to install + + pkgs + List of packages names to install + + args + String or list of extra parameters for zypper + + self_update + Check for newer transactional-update versions. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "continue" to indicate the last snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update pkg_update pkg=emacs snapshot="continue" + + """ + cmd = ["transactional-update"] + cmd.extend(_global_params(self_update=self_update, snapshot=snapshot)) + cmd.extend(["pkg", "update"]) + cmd.extend(_pkg_params(pkg, pkgs, args)) + return _cmd(cmd) + + +def rollback(snapshot=None): + """Set the current, given or last working snapshot as default snapshot + + Sets the default root file system. On a read-only system the root + file system is set directly using btrfs. On read-write systems + snapper(8) rollback is called. + + If no snapshot number is given, the current root file system is + set as the new default root file system. Otherwise number can + either be a snapshot number (as displayed by snapper list) or the + word last. last will try to reset to the latest working snapshot. + + snapshot + Use the given snapshot or, if no number is given, the current + default snapshot as a base for the next snapshot. Use + "last" to indicate the last working snapshot done. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update rollback + + """ + if ( + snapshot + and isinstance(snapshot, str) + and snapshot != "last" + and not snapshot.isnumeric() + ): + raise salt.exceptions.CommandExecutionError( + "snapshot should be a number or 'last'" + ) + cmd = ["transactional-update"] + cmd.append("rollback") + if snapshot: + cmd.append(snapshot) + return _cmd(cmd) + + +def pending_transaction(): + """Check if there is a pending transaction + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update pending_transaction + + """ + # If we are running inside a transaction, we do not have a good + # way yet to detect a pending transaction + if in_transaction(): + raise salt.exceptions.CommandExecutionError( + "pending_transaction cannot be executed inside a transaction" + ) + + cmd = ["snapper", "--no-dbus", "list", "--columns", "number"] + snapshots = _cmd(cmd) + + return any(snapshot.endswith("+") for snapshot in snapshots) + + +def call(function, *args, **kwargs): + """Executes a Salt function inside a transaction. + + The chroot does not need to have Salt installed, but Python is + required. + + function + Salt execution module function + + activate_transaction + If at the end of the transaction there is a pending activation + (i.e there is a new snaphot in the system), a new reboot will + be scheduled (default False) + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update.call test.ping + salt microos transactional_update.call ssh.set_auth_key user key=mykey + salt microos transactional_update.call pkg.install emacs activate_transaction=True + + """ + + if not function: + raise salt.exceptions.CommandExecutionError("Missing function parameter") + + activate_transaction = kwargs.pop("activate_transaction", False) + + # Generate the salt-thin and create a temporary directory in a + # place that the new transaction will have access to, and where we + # can untar salt-thin + thin_path = __utils__["thin.gen_thin"]( + __opts__["cachedir"], + extra_mods=__salt__["config.option"]("thin_extra_mods", ""), + so_mods=__salt__["config.option"]("thin_so_mods", ""), + ) + thin_dest_path = tempfile.mkdtemp(dir=__opts__["cachedir"]) + # Some bug in Salt is preventing us to use `archive.tar` here. A + # AsyncZeroMQReqChannel is not closed at the end of the salt-call, + # and makes the client never exit. + # + # stdout = __salt__['archive.tar']('xzf', thin_path, dest=thin_dest_path) + # + stdout = __salt__["cmd.run"](["tar", "xzf", thin_path, "-C", thin_dest_path]) + if stdout: + __utils__["files.rm_rf"](thin_dest_path) + return {"result": False, "comment": stdout} + + try: + safe_kwargs = salt.utils.args.clean_kwargs(**kwargs) + salt_argv = ( + [ + "python{}".format(sys.version_info[0]), + os.path.join(thin_dest_path, "salt-call"), + "--metadata", + "--local", + "--log-file", + os.path.join(thin_dest_path, "log"), + "--cachedir", + os.path.join(thin_dest_path, "cache"), + "--out", + "json", + "-l", + "quiet", + "--", + function, + ] + + list(args) + + ["{}={}".format(k, v) for (k, v) in safe_kwargs.items()] + ) + try: + ret_stdout = run([str(x) for x in salt_argv], snapshot="continue") + except salt.exceptions.CommandExecutionError as e: + ret_stdout = e.message + + # Process "real" result in stdout + try: + data = __utils__["json.find_json"](ret_stdout) + local = data.get("local", data) + if isinstance(local, dict) and "retcode" in local: + __context__["retcode"] = local["retcode"] + return local.get("return", data) + except ValueError: + return {"result": False, "retcode": 1, "comment": ret_stdout} + finally: + __utils__["files.rm_rf"](thin_dest_path) + + # Check if reboot is needed + if activate_transaction and pending_transaction(): + reboot() + + +def apply_(mods=None, **kwargs): + """Apply an state inside a transaction. + + This function will call `transactional_update.highstate` or + `transactional_update.sls` based on the arguments passed to this + function. It exists as a more intuitive way of applying states. + + For a formal description of the possible parameters accepted in + this function, check `state.apply_` documentation. + + activate_transaction + If at the end of the transaction there is a pending activation + (i.e there is a new snaphot in the system), a new reboot will + be scheduled (default False) + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update.apply + salt microos transactional_update.apply stuff + salt microos transactional_update.apply stuff pillar='{"foo": "bar"}' + salt microos transactional_update.apply stuff activate_transaction=True + + """ + if mods: + return sls(mods, **kwargs) + return highstate(**kwargs) + + +def _create_and_execute_salt_state( + chunks, file_refs, test, hash_type, activate_transaction +): + """Create the salt_state tarball, and execute it in a transaction""" + + # Create the tar containing the state pkg and relevant files. + salt.client.ssh.wrapper.state._cleanup_slsmod_low_data(chunks) + trans_tar = salt.client.ssh.state.prep_trans_tar( + salt.fileclient.get_file_client(__opts__), chunks, file_refs, __pillar__.value() + ) + trans_tar_sum = salt.utils.hashutils.get_hash(trans_tar, hash_type) + + ret = None + + # Create a temporary directory accesible later by the transaction + # where we can move the salt_state.tgz + salt_state_path = tempfile.mkdtemp(dir=__opts__["cachedir"]) + salt_state_path = os.path.join(salt_state_path, "salt_state.tgz") + try: + salt.utils.files.copyfile(trans_tar, salt_state_path) + ret = call( + "state.pkg", + salt_state_path, + test=test, + pkg_sum=trans_tar_sum, + hash_type=hash_type, + activate_transaction=activate_transaction, + ) + finally: + __utils__["files.rm_rf"](salt_state_path) + + return ret + + +def sls( + mods, + saltenv="base", + test=None, + exclude=None, + activate_transaction=False, + queue=False, + **kwargs +): + """Execute the states in one or more SLS files inside a transaction. + + saltenv + Specify a salt fileserver environment to be used when applying + states + + mods + List of states to execute + + test + Run states in test-only (dry-run) mode + + exclude + Exclude specific states from execution. Accepts a list of sls + names, a comma-separated string of sls names, or a list of + dictionaries containing ``sls`` or ``id`` keys. Glob-patterns + may be used to match multiple states. + + activate_transaction + If at the end of the transaction there is a pending activation + (i.e there is a new snaphot in the system), a new reboot will + be scheduled (default False) + + queue + Instead of failing immediately when another state run is in progress, + queue the new state run to begin running once the other has finished. + + This option starts a new thread for each queued state run, so use this + option sparingly. (Default: False) + + For a formal description of the possible parameters accepted in + this function, check `state.sls` documentation. + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update.sls stuff pillar='{"foo": "bar"}' + salt microos transactional_update.sls stuff activate_transaction=True + + """ + conflict = _check_queue(queue, kwargs) + if conflict is not None: + return conflict + + # Get a copy of the pillar data, to avoid overwriting the current + # pillar, instead the one delegated + pillar = copy.deepcopy(__pillar__.value()) + pillar.update(kwargs.get("pillar", {})) + + # Clone the options data and apply some default values. May not be + # needed, as this module just delegate + opts = salt.utils.state.get_sls_opts(__opts__, **kwargs) + st_ = TransactionalUpdateHighstate( + opts, pillar, __salt__, salt.fileclient.get_file_client(__opts__) + ) + + if isinstance(mods, str): + mods = mods.split(",") + + high_data, errors = st_.render_highstate({saltenv: mods}) + if exclude: + if isinstance(exclude, str): + exclude = exclude.split(",") + if "__exclude__" in high_data: + high_data["__exclude__"].extend(exclude) + else: + high_data["__exclude__"] = exclude + + high_data, ext_errors = st_.state.reconcile_extend(high_data) + errors += ext_errors + errors += st_.state.verify_high(high_data) + if errors: + return errors + + high_data, req_in_errors = st_.state.requisite_in(high_data) + errors += req_in_errors + if errors: + return errors + + high_data = st_.state.apply_exclude(high_data) + + # Compile and verify the raw chunks + chunks = st_.state.compile_high_data(high_data) + file_refs = salt.client.ssh.state.lowstate_file_refs( + chunks, + salt.client.ssh.wrapper.state._merge_extra_filerefs( + kwargs.get("extra_filerefs", ""), opts.get("extra_filerefs", "") + ), + ) + + hash_type = opts["hash_type"] + return _create_and_execute_salt_state( + chunks, file_refs, test, hash_type, activate_transaction + ) + + +def highstate(activate_transaction=False, queue=False, **kwargs): + """Retrieve the state data from the salt master for this minion and + execute it inside a transaction. + + For a formal description of the possible parameters accepted in + this function, check `state.highstate` documentation. + + activate_transaction + If at the end of the transaction there is a pending activation + (i.e there is a new snaphot in the system), a new reboot will + be scheduled (default False) + + queue + Instead of failing immediately when another state run is in progress, + queue the new state run to begin running once the other has finished. + + This option starts a new thread for each queued state run, so use this + option sparingly. (Default: False) + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update.highstate + salt microos transactional_update.highstate pillar='{"foo": "bar"}' + salt microos transactional_update.highstate activate_transaction=True + + """ + conflict = _check_queue(queue, kwargs) + if conflict is not None: + return conflict + + # Get a copy of the pillar data, to avoid overwriting the current + # pillar, instead the one delegated + pillar = copy.deepcopy(__pillar__.value()) + pillar.update(kwargs.get("pillar", {})) + + # Clone the options data and apply some default values. May not be + # needed, as this module just delegate + opts = salt.utils.state.get_sls_opts(__opts__, **kwargs) + st_ = TransactionalUpdateHighstate( + opts, pillar, __salt__, salt.fileclient.get_file_client(__opts__) + ) + + # Compile and verify the raw chunks + chunks = st_.compile_low_chunks() + file_refs = salt.client.ssh.state.lowstate_file_refs( + chunks, + salt.client.ssh.wrapper.state._merge_extra_filerefs( + kwargs.get("extra_filerefs", ""), opts.get("extra_filerefs", "") + ), + ) + # Check for errors + for chunk in chunks: + if not isinstance(chunk, dict): + __context__["retcode"] = 1 + return chunks + + test = kwargs.pop("test", False) + hash_type = opts["hash_type"] + return _create_and_execute_salt_state( + chunks, file_refs, test, hash_type, activate_transaction + ) + + +def single(fun, name, test=None, activate_transaction=False, queue=False, **kwargs): + """Execute a single state function with the named kwargs, returns + False if insufficient data is sent to the command + + By default, the values of the kwargs will be parsed as YAML. So, + you can specify lists values, or lists of single entry key-value + maps, as you would in a YAML salt file. Alternatively, JSON format + of keyword values is also supported. + + activate_transaction + If at the end of the transaction there is a pending activation + (i.e there is a new snaphot in the system), a new reboot will + be scheduled (default False) + + queue + Instead of failing immediately when another state run is in progress, + queue the new state run to begin running once the other has finished. + + This option starts a new thread for each queued state run, so use this + option sparingly. (Default: False) + + CLI Example: + + .. code-block:: bash + + salt microos transactional_update.single pkg.installed name=emacs + salt microos transactional_update.single pkg.installed name=emacs activate_transaction=True + + """ + conflict = _check_queue(queue, kwargs) + if conflict is not None: + return conflict + + # Get a copy of the pillar data, to avoid overwriting the current + # pillar, instead the one delegated + pillar = copy.deepcopy(__pillar__.value()) + pillar.update(kwargs.get("pillar", {})) + + # Clone the options data and apply some default values. May not be + # needed, as this module just delegate + opts = salt.utils.state.get_sls_opts(__opts__, **kwargs) + st_ = salt.client.ssh.state.SSHState(opts, pillar) + + # state.fun -> [state, fun] + comps = fun.split(".") + if len(comps) < 2: + __context__["retcode"] = 1 + return "Invalid function passed" + + # Create the low chunk, using kwargs as a base + kwargs.update({"state": comps[0], "fun": comps[1], "__id__": name, "name": name}) + + # Verify the low chunk + err = st_.verify_data(kwargs) + if err: + __context__["retcode"] = 1 + return err + + # Must be a list of low-chunks + chunks = [kwargs] + + # Retrieve file refs for the state run, so we can copy relevant + # files down to the minion before executing the state + file_refs = salt.client.ssh.state.lowstate_file_refs( + chunks, + salt.client.ssh.wrapper.state._merge_extra_filerefs( + kwargs.get("extra_filerefs", ""), opts.get("extra_filerefs", "") + ), + ) + + hash_type = opts["hash_type"] + return _create_and_execute_salt_state( + chunks, file_refs, test, hash_type, activate_transaction + ) diff --git a/salt/states/service.py b/salt/states/service.py index 27595f7703..e11d773965 100644 --- a/salt/states/service.py +++ b/salt/states/service.py @@ -345,6 +345,10 @@ def _disable(name, started, result=True, **kwargs): return ret +def _offline(): + return "service.offline" in __salt__ and __salt__["service.offline"]() + + def _available(name, ret): """ Check if the service is available @@ -439,6 +443,11 @@ def running(name, enable=None, sig=None, init_delay=None, **kwargs): if isinstance(enable, str): enable = salt.utils.data.is_true(enable) + if _offline(): + ret["result"] = True + ret["comment"] = "Running in OFFLINE mode. Nothing to do" + return ret + # Check if the service is available try: if not _available(name, ret): @@ -634,6 +643,11 @@ def dead(name, enable=None, sig=None, init_delay=None, **kwargs): if isinstance(enable, str): enable = salt.utils.data.is_true(enable) + if _offline(): + ret["result"] = True + ret["comment"] = "Running in OFFLINE mode. Nothing to do" + return ret + # Check if the service is available try: if not _available(name, ret): diff --git a/salt/utils/systemd.py b/salt/utils/systemd.py index 85a9c9172c..702e06147a 100644 --- a/salt/utils/systemd.py +++ b/salt/utils/systemd.py @@ -7,7 +7,7 @@ import os import re import subprocess -import salt.loader_context +import salt.utils.path import salt.utils.stringutils from salt.exceptions import SaltInvocationError @@ -51,6 +51,30 @@ def booted(context=None): return ret +def offline(context=None): + """Return True if systemd is in offline mode + + .. versionadded:: 3004 + """ + contextkey = "salt.utils.systemd.offline" + if isinstance(context, (dict, salt.loader_context.NamedLoaderContext)): + if contextkey in context: + return context[contextkey] + elif context is not None: + raise SaltInvocationError("context must be a dictionary if passed") + + # Note that there is a difference from SYSTEMD_OFFLINE=1. Here we + # assume that there is no PID 1 to talk with. + ret = not booted(context) and salt.utils.path.which("systemctl") + + try: + context[contextkey] = ret + except TypeError: + pass + + return ret + + def version(context=None): """ Attempts to run systemctl --version. Returns None if unable to determine diff --git a/tests/integration/states/test_service.py b/tests/integration/states/test_service.py index 9aa393b630..b775e6765a 100644 --- a/tests/integration/states/test_service.py +++ b/tests/integration/states/test_service.py @@ -25,6 +25,7 @@ class ServiceTest(ModuleCase, SaltReturnAssertsMixin): cmd_name = "crontab" os_family = self.run_function("grains.get", ["os_family"]) os_release = self.run_function("grains.get", ["osrelease"]) + is_systemd = self.run_function("grains.get", ["systemd"]) self.stopped = False self.running = True if os_family == "RedHat": @@ -52,6 +53,9 @@ class ServiceTest(ModuleCase, SaltReturnAssertsMixin): if os_family != "Windows" and salt.utils.path.which(cmd_name) is None: self.skipTest("{} is not installed".format(cmd_name)) + if is_systemd and self.run_function("service.offline"): + self.skipTest("systemd is OFFLINE") + def tearDown(self): if self.post_srv_disable: self.run_function("service.disable", name=self.service_name) diff --git a/tests/pytests/unit/modules/test_rebootmgr.py b/tests/pytests/unit/modules/test_rebootmgr.py new file mode 100644 index 0000000000..a2ceb65cd9 --- /dev/null +++ b/tests/pytests/unit/modules/test_rebootmgr.py @@ -0,0 +1,309 @@ +import pytest +import salt.modules.rebootmgr as rebootmgr +from salt.exceptions import CommandExecutionError +from tests.support.mock import MagicMock, patch + + +@pytest.fixture +def configure_loader_modules(): + return {rebootmgr: {"__salt__": {}, "__utils__": {}}} + + +def test_version(): + """ + Test rebootmgr.version without parameters + """ + version = "rebootmgrctl (rebootmgr) 1.3" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": version, "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.version() == "1.3" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "--version"]) + + +def test_is_active(): + """ + Test rebootmgr.is_active without parameters + """ + salt_mock = {"cmd.run_all": MagicMock(return_value={"stdout": None, "retcode": 0})} + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.is_active() + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "is_active", "--quiet"] + ) + + +def test_reboot(): + """ + Test rebootmgr.reboot without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.reboot() == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "reboot"]) + + +def test_reboot_order(): + """ + Test rebootmgr.reboot with order parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.reboot("now") == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "reboot", "now"]) + + +def test_reboot_invalid(): + """ + Test rebootmgr.reboot with invalid parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + with pytest.raises(CommandExecutionError): + rebootmgr.reboot("invalid") + + +def test_cancel(): + """ + Test rebootmgr.cancel without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.cancel() == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "cancel"]) + + +def test_status(): + """ + Test rebootmgr.status without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + # 0 - No reboot requested + assert rebootmgr.status() == 0 + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "status", "--quiet"] + ) + + +def test_set_strategy_default(): + """ + Test rebootmgr.set_strategy without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_strategy() == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "set-strategy"]) + + +def test_set_strategy(): + """ + Test rebootmgr.set_strategy with strategy parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_strategy("best-effort") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "set-strategy", "best-effort"] + ) + + +def test_set_strategy_invalid(): + """ + Test rebootmgr.strategy with invalid parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + with pytest.raises(CommandExecutionError): + rebootmgr.set_strategy("invalid") + + +def test_get_strategy(): + """ + Test rebootmgr.get_strategy without parameters + """ + strategy = "Reboot strategy: best-effort" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": strategy, "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.get_strategy() == "best-effort" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "get-strategy"]) + + +def test_set_window(): + """ + Test rebootmgr.set_window with parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_window("Thu,Fri 2020-*-1,5 11:12:13", "1h") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "set-window", "Thu,Fri 2020-*-1,5 11:12:13", "1h"] + ) + + +def test_get_window(): + """ + Test rebootmgr.get_window without parameters + """ + window = "Maintenance window is set to *-*-* 03:30:00, lasting 01h30m." + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": window, "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.get_window() == { + "time": "*-*-* 03:30:00", + "duration": "01h30m", + } + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "get-window"]) + + +def test_set_group(): + """ + Test rebootmgr.set_group with parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_group("group1") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "set-group", "group1"] + ) + + +def test_get_group(): + """ + Test rebootmgr.get_group without parameters + """ + group = "Etcd lock group is set to group1" + salt_mock = {"cmd.run_all": MagicMock(return_value={"stdout": group, "retcode": 0})} + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.get_group() == "group1" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "get-group"]) + + +def test_set_max(): + """ + Test rebootmgr.set_max with default parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_max(10) == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "set-max", 10]) + + +def test_set_max_group(): + """ + Test rebootmgr.set_max with group parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.set_max(10, "group1") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "set-max", "--group", "group1", 10] + ) + + +def test_lock(): + """ + Test rebootmgr.lock without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.lock() == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "lock"]) + + +def test_lock_machine_id(): + """ + Test rebootmgr.lock with machine_id parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.lock("machine-id") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "lock", "machine-id"] + ) + + +def test_lock_machine_id_group(): + """ + Test rebootmgr.lock with machine_id and group parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.lock("machine-id", "group1") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "lock", "--group", "group1", "machine-id"] + ) + + +def test_unlock(): + """ + Test rebootmgr.unlock without parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.unlock() == "output" + salt_mock["cmd.run_all"].assert_called_with(["rebootmgrctl", "unlock"]) + + +def test_unlock_machine_id(): + """ + Test rebootmgr.unlock with machine_id parameter + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.unlock("machine-id") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "unlock", "machine-id"] + ) + + +def test_unlock_machine_id_group(): + """ + Test rebootmgr.unlock with machine_id and group parameters + """ + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(rebootmgr.__salt__, salt_mock): + assert rebootmgr.unlock("machine-id", "group1") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["rebootmgrctl", "unlock", "--group", "group1", "machine-id"] + ) diff --git a/tests/pytests/unit/modules/test_transactional_update.py b/tests/pytests/unit/modules/test_transactional_update.py new file mode 100644 index 0000000000..e7293cf3e2 --- /dev/null +++ b/tests/pytests/unit/modules/test_transactional_update.py @@ -0,0 +1,999 @@ +import sys + +import pytest +import salt.loader_context +import salt.modules.state as statemod +import salt.modules.transactional_update as tu +from salt.exceptions import CommandExecutionError +from tests.support.mock import MagicMock, patch + +pytestmark = [ + pytest.mark.skip_on_windows(reason="Not supported on Windows"), +] + + +@pytest.fixture +def configure_loader_modules(): + loader_context = salt.loader_context.LoaderContext() + return { + tu: { + "__salt__": {}, + "__utils__": {}, + "__pillar__": salt.loader_context.NamedLoaderContext( + "__pillar__", loader_context, {} + ), + }, + statemod: {"__salt__": {}, "__context__": {}}, + } + + +def test__global_params_no_self_update(): + """Test transactional_update._global_params without self_update""" + assert tu._global_params(self_update=False) == [ + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + ] + + +def test__global_params_self_update(): + """Test transactional_update._global_params with self_update""" + assert tu._global_params(self_update=True) == [ + "--non-interactive", + "--drop-if-no-change", + ] + + +def test__global_params_no_self_update_snapshot(): + """Test transactional_update._global_params without self_update and + snapshot + + """ + assert tu._global_params(self_update=False, snapshot=10) == [ + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + 10, + ] + + +def test__global_params_no_self_update_continue(): + """Test transactional_update._global_params without self_update and + snapshot conitue + + """ + assert tu._global_params(self_update=False, snapshot="continue") == [ + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + ] + + +def test__pkg_params_no_packages(): + """Test transactional_update._pkg_params without packages""" + with pytest.raises(CommandExecutionError): + tu._pkg_params(pkg=None, pkgs=None, args=None) + + +def test__pkg_params_pkg(): + """Test transactional_update._pkg_params with single package""" + assert tu._pkg_params(pkg="pkg1", pkgs=None, args=None) == ["pkg1"] + + +def test__pkg_params_pkgs(): + """Test transactional_update._pkg_params with packages""" + assert tu._pkg_params(pkg=None, pkgs="pkg1", args=None) == ["pkg1"] + assert tu._pkg_params(pkg=None, pkgs="pkg1 pkg2 ", args=None) == [ + "pkg1", + "pkg2", + ] + assert tu._pkg_params(pkg=None, pkgs=["pkg1", "pkg2"], args=None) == [ + "pkg1", + "pkg2", + ] + + +def test__pkg_params_pkg_pkgs(): + """Test transactional_update._pkg_params with packages""" + assert tu._pkg_params(pkg="pkg1", pkgs="pkg2", args=None) == [ + "pkg1", + "pkg2", + ] + + +def test__pkg_params_args(): + """Test transactional_update._pkg_params with argumens""" + assert tu._pkg_params(pkg="pkg1", pkgs=None, args="--arg1") == [ + "--arg1", + "pkg1", + ] + assert tu._pkg_params(pkg="pkg1", pkgs=None, args="--arg1 --arg2") == [ + "--arg1", + "--arg2", + "pkg1", + ] + assert tu._pkg_params(pkg="pkg1", pkgs=None, args=["--arg1", "--arg2"]) == [ + "--arg1", + "--arg2", + "pkg1", + ] + + +def test_transactional_transactional(): + """Test transactional_update.transactional""" + matrix = (("/usr/sbin/transactional-update", True), ("", False)) + + for path_which, result in matrix: + utils_mock = {"path.which": MagicMock(return_value=path_which)} + + with patch.dict(tu.__utils__, utils_mock): + assert tu.transactional() is result + utils_mock["path.which"].assert_called_with("transactional-update") + + +def test_in_transaction(): + """Test transactional_update.in_transaction""" + matrix = ( + ("/usr/sbin/transactional-update", True, True), + ("/usr/sbin/transactional-update", False, False), + ("", True, False), + ("", False, False), + ) + + for path_which, in_chroot, result in matrix: + utils_mock = {"path.which": MagicMock(return_value=path_which)} + salt_mock = {"chroot.in_chroot": MagicMock(return_value=in_chroot)} + + with patch.dict(tu.__utils__, utils_mock): + with patch.dict(tu.__salt__, salt_mock): + assert tu.in_transaction() is result + + +def test_commands_with_global_params(): + """Test commands that only accept global params""" + for cmd in [ + "cleanup", + "cleanup_snapshots", + "cleanup_overlays", + "grub_cfg", + "bootloader", + "initrd", + "kdump", + "reboot", + "dup", + "up", + "patch", + "migration", + ]: + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert getattr(tu, cmd)() == "output" + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + cmd.replace("_", ".") + if cmd.startswith("grub") + else cmd.replace("_", "-"), + ] + ) + + +def test_run_error(): + """Test transactional_update.run with missing command""" + with pytest.raises(CommandExecutionError): + tu.run(None) + + +def test_run_string(): + """Test transactional_update.run with command as string""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.run("cmd --flag p1 p2") == "output" + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--quiet", + "run", + "cmd", + "--flag", + "p1", + "p2", + ] + ) + + +def test_run_array(): + """Test transactional_update.run with command as array""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.run(["cmd", "--flag", "p1", "p2"]) == "output" + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--quiet", + "run", + "cmd", + "--flag", + "p1", + "p2", + ] + ) + + +def test_pkg_commands(): + """Test transactional_update.pkg_* commands""" + for cmd in ["pkg_install", "pkg_remove", "pkg_update"]: + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert getattr(tu, cmd)("pkg1", "pkg2 pkg3", "--arg") == "output" + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "pkg", + cmd.replace("pkg_", ""), + "--arg", + "pkg1", + "pkg2", + "pkg3", + ] + ) + + +def test_rollback_error(): + """Test transactional_update.rollback with wrong snapshot""" + with pytest.raises(CommandExecutionError): + tu.rollback("error") + + +def test_rollback_default(): + """Test transactional_update.rollback with default snapshot""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.rollback() == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["transactional-update", "rollback"] + ) + + +def test_rollback_snapshot_number(): + """Test transactional_update.rollback with numeric snapshot""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.rollback(10) == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["transactional-update", "rollback", 10] + ) + + +def test_rollback_snapshot_str(): + """Test transactional_update.rollback with string snapshot""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.rollback("10") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["transactional-update", "rollback", "10"] + ) + + +def test_rollback_last(): + """Test transactional_update.rollback with last snapshot""" + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": "output", "retcode": 0}) + } + with patch.dict(tu.__salt__, salt_mock): + assert tu.rollback("last") == "output" + salt_mock["cmd.run_all"].assert_called_with( + ["transactional-update", "rollback", "last"] + ) + + +def test_pending_transaction(): + """Test transactional_update.pending_transaction""" + matrix = ( + (False, ["1", "2+", "3-"], True), + (False, ["1", "2-", "3+"], True), + (False, ["1", "2", "3*"], False), + ) + + for in_transaction, snapshots, result in matrix: + salt_mock = { + "cmd.run_all": MagicMock(return_value={"stdout": snapshots, "retcode": 0}) + } + + tu_in_transaction = "salt.modules.transactional_update.in_transaction" + with patch(tu_in_transaction) as in_transaction_mock: + in_transaction_mock.return_value = in_transaction + with patch.dict(tu.__salt__, salt_mock): + assert tu.pending_transaction() is result + salt_mock["cmd.run_all"].assert_called_with( + ["snapper", "--no-dbus", "list", "--columns", "number"] + ) + + +def test_pending_transaction_in_transaction(): + """Test transactional_update.pending_transaction when in transaction""" + tu_in_transaction = "salt.modules.transactional_update.in_transaction" + with patch(tu_in_transaction) as in_transaction_mock: + in_transaction_mock.return_value = True + with pytest.raises(CommandExecutionError): + tu.pending_transaction() + + +def test_call_fails_input_validation(): + """Test transactional_update.call missing function name""" + with pytest.raises(CommandExecutionError): + tu.call("") + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_fails_untar(): + """Test transactional_update.call when tar fails""" + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value="Error"), + "config.option": MagicMock(), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock): + assert tu.call("/chroot", "test.ping") == { + "result": False, + "comment": "Error", + } + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + utils_mock["files.rm_rf"].assert_called_once() + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_fails_salt_thin(): + """Test transactional_update.chroot when fails salt_thin""" + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + "json.find_json": MagicMock(side_effect=ValueError()), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value=""), + "config.option": MagicMock(), + "cmd.run_all": MagicMock(return_value={"retcode": 1, "stderr": "Error"}), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock): + assert tu.call("test.ping") == { + "result": False, + "retcode": 1, + "comment": "Error", + } + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + "--quiet", + "run", + "python{}".format(sys.version_info[0]), + "/var/cache/salt/minion/tmp01/salt-call", + "--metadata", + "--local", + "--log-file", + "/var/cache/salt/minion/tmp01/log", + "--cachedir", + "/var/cache/salt/minion/tmp01/cache", + "--out", + "json", + "-l", + "quiet", + "--", + "test.ping", + ] + ) + utils_mock["files.rm_rf"].assert_called_once() + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_fails_function(): + """Test transactional_update.chroot when fails the function""" + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + "json.find_json": MagicMock(side_effect=ValueError()), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value=""), + "config.option": MagicMock(), + "cmd.run_all": MagicMock( + return_value={"retcode": 0, "stdout": "Not found", "stderr": ""} + ), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock): + assert tu.call("test.ping") == { + "result": False, + "retcode": 1, + "comment": "Not found", + } + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + "--quiet", + "run", + "python{}".format(sys.version_info[0]), + "/var/cache/salt/minion/tmp01/salt-call", + "--metadata", + "--local", + "--log-file", + "/var/cache/salt/minion/tmp01/log", + "--cachedir", + "/var/cache/salt/minion/tmp01/cache", + "--out", + "json", + "-l", + "quiet", + "--", + "test.ping", + ] + ) + utils_mock["files.rm_rf"].assert_called_once() + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_success_no_reboot(): + """Test transactional_update.chroot when succeed""" + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + "json.find_json": MagicMock(return_value={"return": "result"}), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value=""), + "config.option": MagicMock(), + "cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock): + assert tu.call("test.ping") == "result" + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + "--quiet", + "run", + "python{}".format(sys.version_info[0]), + "/var/cache/salt/minion/tmp01/salt-call", + "--metadata", + "--local", + "--log-file", + "/var/cache/salt/minion/tmp01/log", + "--cachedir", + "/var/cache/salt/minion/tmp01/cache", + "--out", + "json", + "-l", + "quiet", + "--", + "test.ping", + ] + ) + utils_mock["files.rm_rf"].assert_called_once() + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_success_reboot(): + """Test transactional_update.chroot when succeed and reboot""" + pending_transaction_mock = MagicMock(return_value=True) + reboot_mock = MagicMock() + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + "json.find_json": MagicMock(return_value={"return": "result"}), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value=""), + "config.option": MagicMock(), + "cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock), patch( + "salt.modules.transactional_update.pending_transaction", + pending_transaction_mock, + ), patch( + "salt.modules.transactional_update.reboot", reboot_mock + ): + assert ( + tu.call("transactional_update.dup", activate_transaction=True) == "result" + ) + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + "--quiet", + "run", + "python{}".format(sys.version_info[0]), + "/var/cache/salt/minion/tmp01/salt-call", + "--metadata", + "--local", + "--log-file", + "/var/cache/salt/minion/tmp01/log", + "--cachedir", + "/var/cache/salt/minion/tmp01/cache", + "--out", + "json", + "-l", + "quiet", + "--", + "transactional_update.dup", + ] + ) + utils_mock["files.rm_rf"].assert_called_once() + pending_transaction_mock.assert_called_once() + reboot_mock.assert_called_once() + + +@patch("tempfile.mkdtemp", MagicMock(return_value="/var/cache/salt/minion/tmp01")) +def test_call_success_parameters(): + """Test transactional_update.chroot when succeed with parameters""" + utils_mock = { + "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), + "files.rm_rf": MagicMock(), + "json.find_json": MagicMock(return_value={"return": "result"}), + } + opts_mock = {"cachedir": "/var/cache/salt/minion"} + salt_mock = { + "cmd.run": MagicMock(return_value=""), + "config.option": MagicMock(), + "cmd.run_all": MagicMock(return_value={"retcode": 0, "stdout": ""}), + } + with patch.dict(tu.__utils__, utils_mock), patch.dict( + tu.__opts__, opts_mock + ), patch.dict(tu.__salt__, salt_mock): + assert tu.call("module.function", key="value") == "result" + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() + salt_mock["cmd.run"].assert_called_once() + salt_mock["cmd.run_all"].assert_called_with( + [ + "transactional-update", + "--non-interactive", + "--drop-if-no-change", + "--no-selfupdate", + "--continue", + "--quiet", + "run", + "python{}".format(sys.version_info[0]), + "/var/cache/salt/minion/tmp01/salt-call", + "--metadata", + "--local", + "--log-file", + "/var/cache/salt/minion/tmp01/log", + "--cachedir", + "/var/cache/salt/minion/tmp01/cache", + "--out", + "json", + "-l", + "quiet", + "--", + "module.function", + "key=value", + ] + ) + utils_mock["files.rm_rf"].assert_called_once() + + +def test_sls(): + """Test transactional_update.sls""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + transactional_update_highstate_mock.render_highstate.return_value = (None, []) + transactional_update_highstate_mock.state.reconcile_extend.return_value = (None, []) + transactional_update_highstate_mock.state.requisite_in.return_value = (None, []) + transactional_update_highstate_mock.state.verify_high.return_value = [] + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock(return_value=[]), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.sls("module") == "result" + _create_and_execute_salt_state_mock.assert_called_once() + + +def test_sls_queue_true(): + """Test transactional_update.sls""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + transactional_update_highstate_mock.render_highstate.return_value = (None, []) + transactional_update_highstate_mock.state.reconcile_extend.return_value = (None, []) + transactional_update_highstate_mock.state.requisite_in.return_value = (None, []) + transactional_update_highstate_mock.state.verify_high.return_value = [] + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.sls("module", queue=True) == "result" + _create_and_execute_salt_state_mock.assert_called_once() + + +def test_sls_queue_false_failing(): + """Test transactional_update.sls""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + transactional_update_highstate_mock.render_highstate.return_value = (None, []) + transactional_update_highstate_mock.state.reconcile_extend.return_value = (None, []) + transactional_update_highstate_mock.state.requisite_in.return_value = (None, []) + transactional_update_highstate_mock.state.verify_high.return_value = [] + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.sls("module", queue=False) == [ + 'The function "state.running" is running as PID 4126 and was started at 2015, Mar 25 12:34:07.204096 with jid 20150325123407204096' + ] + _create_and_execute_salt_state_mock.assert_not_called() + + +def test_highstate(): + """Test transactional_update.highstage""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock(return_value=[]), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.highstate() == "result" + _create_and_execute_salt_state_mock.assert_called_once() + + +def test_highstate_queue_true(): + """Test transactional_update.highstage""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.highstate(queue=True) == "result" + _create_and_execute_salt_state_mock.assert_called_once() + + +def test_highstate_queue_false_failing(): + """Test transactional_update.highstage""" + transactional_update_highstate_mock = MagicMock() + transactional_update_highstate_mock.return_value = ( + transactional_update_highstate_mock + ) + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.modules.transactional_update.TransactionalUpdateHighstate", + transactional_update_highstate_mock, + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.highstate(queue=False) == [ + 'The function "state.running" is running as PID 4126 and was started at 2015, Mar 25 12:34:07.204096 with jid 20150325123407204096' + ] + _create_and_execute_salt_state_mock.assert_not_called() + + +def test_single(): + """Test transactional_update.single""" + ssh_state_mock = MagicMock() + ssh_state_mock.return_value = ssh_state_mock + ssh_state_mock.verify_data.return_value = None + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock(return_value=[]), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.client.ssh.state.SSHState", ssh_state_mock + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.single("pkg.installed", name="emacs") == "result" + _create_and_execute_salt_state_mock.assert_called_once() + + +def test_single_queue_false_failing(): + """Test transactional_update.single""" + ssh_state_mock = MagicMock() + ssh_state_mock.return_value = ssh_state_mock + ssh_state_mock.verify_data.return_value = None + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.client.ssh.state.SSHState", ssh_state_mock + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.single("pkg.installed", name="emacs", queue=False) == [ + 'The function "state.running" is running as PID 4126 and was started at 2015, Mar 25 12:34:07.204096 with jid 20150325123407204096' + ] + _create_and_execute_salt_state_mock.assert_not_called() + + +def test_single_queue_true(): + """Test transactional_update.single""" + ssh_state_mock = MagicMock() + ssh_state_mock.return_value = ssh_state_mock + ssh_state_mock.verify_data.return_value = None + + _create_and_execute_salt_state_mock = MagicMock(return_value="result") + opts_mock = { + "hash_type": "md5", + } + salt_mock = { + "saltutil.is_running": MagicMock( + side_effect=[ + [ + { + "fun": "state.running", + "pid": "4126", + "jid": "20150325123407204096", + } + ], + [], + ] + ), + } + get_sls_opts_mock = MagicMock(return_value=opts_mock) + with patch.dict(tu.__opts__, opts_mock), patch.dict( + statemod.__salt__, salt_mock + ), patch("salt.utils.state.get_sls_opts", get_sls_opts_mock), patch( + "salt.fileclient.get_file_client", MagicMock() + ), patch( + "salt.client.ssh.state.SSHState", ssh_state_mock + ), patch( + "salt.modules.transactional_update._create_and_execute_salt_state", + _create_and_execute_salt_state_mock, + ): + assert tu.single("pkg.installed", name="emacs", queue=True) == "result" + _create_and_execute_salt_state_mock.assert_called_once() diff --git a/tests/pytests/unit/states/test_service.py b/tests/pytests/unit/states/test_service.py index 16deafdbe9..1006aee317 100644 --- a/tests/pytests/unit/states/test_service.py +++ b/tests/pytests/unit/states/test_service.py @@ -316,6 +316,22 @@ def test_running(): assert service.__context__ == {"service.state": "running"} +def test_running_in_offline_mode(): + """ + Tests the case in which a service.running state is executed on an offline environemnt + + """ + name = "thisisnotarealservice" + with patch.object(service, "_offline", MagicMock(return_value=True)): + ret = service.running(name=name) + assert ret == { + "changes": {}, + "comment": "Running in OFFLINE mode. Nothing to do", + "result": True, + "name": name, + } + + def test_dead(): """ Test to ensure that the named service is dead @@ -454,6 +470,22 @@ def test_dead_with_missing_service(): } +def test_dead_in_offline_mode(): + """ + Tests the case in which a service.dead state is executed on an offline environemnt + + """ + name = "thisisnotarealservice" + with patch.object(service, "_offline", MagicMock(return_value=True)): + ret = service.dead(name=name) + assert ret == { + "changes": {}, + "comment": "Running in OFFLINE mode. Nothing to do", + "result": True, + "name": name, + } + + def test_enabled(): """ Test to verify that the service is enabled @@ -664,6 +696,8 @@ def test_running_with_reload(): service.__utils__, utils ), patch.dict( service.__opts__, {"test": False} + ), patch( + "salt.utils.systemd.offline", MagicMock(return_value=False) ): service.dead(service_name, enable=False) result = service.running(name=service_name, enable=True, reload=False) diff --git a/tests/unit/modules/test_chroot.py b/tests/unit/modules/test_chroot.py index 76811df46e..9480f3aa7a 100644 --- a/tests/unit/modules/test_chroot.py +++ b/tests/unit/modules/test_chroot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Author: Alberto Planas # @@ -26,16 +25,13 @@ :platform: Linux """ -# Import Python Libs -from __future__ import absolute_import, print_function, unicode_literals import sys +import salt.loader_context import salt.modules.chroot as chroot import salt.utils.platform from salt.exceptions import CommandExecutionError - -# Import Salt Testing Libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.mock import MagicMock, patch from tests.support.unit import TestCase, skipIf @@ -48,7 +44,17 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin): """ def setup_loader_modules(self): - return {chroot: {"__salt__": {}, "__utils__": {}, "__opts__": {"cachedir": ""}}} + loader_context = salt.loader_context.LoaderContext() + return { + chroot: { + "__salt__": {}, + "__utils__": {}, + "__opts__": {"cachedir": ""}, + "__pillar__": salt.loader_context.NamedLoaderContext( + "__pillar__", loader_context, {} + ), + } + } @patch("os.path.isdir") def test_exist(self, isdir): @@ -75,6 +81,17 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin): self.assertTrue(chroot.create("/chroot")) makedirs.assert_called() + @patch("salt.utils.files.fopen") + def test_in_chroot(self, fopen): + """ + Test the detection of chroot environment. + """ + matrix = (("a", "b", True), ("a", "a", False)) + for root_mountinfo, self_mountinfo, result in matrix: + fopen.return_value.__enter__.return_value = fopen + fopen.read = MagicMock(side_effect=(root_mountinfo, self_mountinfo)) + self.assertEqual(chroot.in_chroot(), result) + @patch("salt.modules.chroot.exist") def test_call_fails_input_validation(self, exist): """ @@ -126,19 +143,25 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin): utils_mock = { "thin.gen_thin": MagicMock(return_value="/salt-thin.tgz"), "files.rm_rf": MagicMock(), - "json.find_json": MagicMock(return_value={"return": {}}), + "json.find_json": MagicMock(side_effect=ValueError()), } salt_mock = { "cmd.run": MagicMock(return_value=""), "config.option": MagicMock(), - "cmd.run_chroot": MagicMock(return_value={"retcode": 1, "stderr": "Error"}), + "cmd.run_chroot": MagicMock( + return_value={"retcode": 1, "stdout": "", "stderr": "Error"} + ), } with patch.dict(chroot.__utils__, utils_mock), patch.dict( chroot.__salt__, salt_mock ): self.assertEqual( chroot.call("/chroot", "test.ping"), - {"result": False, "comment": "Can't parse container command output"}, + { + "result": False, + "retcode": 1, + "comment": {"stdout": "", "stderr": "Error"}, + }, ) utils_mock["thin.gen_thin"].assert_called_once() salt_mock["config.option"].assert_called() diff --git a/tests/unit/modules/test_systemd_service.py b/tests/unit/modules/test_systemd_service.py index 65ca30e42d..ffaa05efe6 100644 --- a/tests/unit/modules/test_systemd_service.py +++ b/tests/unit/modules/test_systemd_service.py @@ -243,21 +243,27 @@ class SystemdTestCase(TestCase, LoaderModuleMockMixin): # systemd < 231 with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 230}): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertTrue(systemd.available("sshd.service")) self.assertFalse(systemd.available("foo.service")) # systemd >= 231 with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 231}): with patch.dict(_SYSTEMCTL_STATUS, _SYSTEMCTL_STATUS_GTE_231): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertTrue(systemd.available("sshd.service")) self.assertFalse(systemd.available("bar.service")) # systemd < 231 with retcode/output changes backported (e.g. RHEL 7.3) with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 219}): with patch.dict(_SYSTEMCTL_STATUS, _SYSTEMCTL_STATUS_GTE_231): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertTrue(systemd.available("sshd.service")) self.assertFalse(systemd.available("bar.service")) @@ -269,21 +275,27 @@ class SystemdTestCase(TestCase, LoaderModuleMockMixin): # systemd < 231 with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 230}): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertFalse(systemd.missing("sshd.service")) self.assertTrue(systemd.missing("foo.service")) # systemd >= 231 with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 231}): with patch.dict(_SYSTEMCTL_STATUS, _SYSTEMCTL_STATUS_GTE_231): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertFalse(systemd.missing("sshd.service")) self.assertTrue(systemd.missing("bar.service")) # systemd < 231 with retcode/output changes backported (e.g. RHEL 7.3) with patch.dict(systemd.__context__, {"salt.utils.systemd.version": 219}): with patch.dict(_SYSTEMCTL_STATUS, _SYSTEMCTL_STATUS_GTE_231): - with patch.object(systemd, "_systemctl_status", mock): + with patch.object(systemd, "_systemctl_status", mock), patch.object( + systemd, "offline", MagicMock(return_value=False) + ): self.assertFalse(systemd.missing("sshd.service")) self.assertTrue(systemd.missing("bar.service")) -- 2.33.0