d0e1a8d380
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=190
3986 lines
130 KiB
Diff
3986 lines
130 KiB
Diff
From 686529e73fb099af90ebce9a6c5e1f432c6484f9 Mon Sep 17 00:00:00 2001
|
|
From: Alberto Planas <aplanas@suse.com>
|
|
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 <aplanas@suse.com>
|
|
: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 <aplanas@suse.com>
|
|
+: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 <command>``, where ``<command>``
|
|
+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 <aplanas@suse.com>
|
|
+: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 <aplanas@suse.com>
|
|
#
|
|
@@ -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
|
|
|
|
|