65598582f5
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=179
3202 lines
105 KiB
Diff
3202 lines
105 KiB
Diff
From aa0d6604a7c6e2a25e88679ec64855723e6cabbf Mon Sep 17 00:00:00 2001
|
|
From: Alberto Planas <aplanas@suse.com>
|
|
Date: Mon, 5 Oct 2020 16:32:44 +0200
|
|
Subject: [PATCH] Support transactional systems (MicroOS) (#271)
|
|
|
|
* 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
|
|
---
|
|
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 +
|
|
salt/executors/transactional_update.py | 126 ++
|
|
salt/grains/extra.py | 42 +-
|
|
salt/modules/chroot.py | 46 +-
|
|
salt/modules/rebootmgr.py | 357 +++++
|
|
salt/modules/systemd_service.py | 21 +-
|
|
salt/modules/transactional_update.py | 1270 +++++++++++++++++
|
|
salt/utils/systemd.py | 27 +-
|
|
tests/unit/modules/test_chroot.py | 18 +-
|
|
tests/unit/modules/test_rebootmgr.py | 302 ++++
|
|
.../unit/modules/test_transactional_update.py | 681 +++++++++
|
|
16 files changed, 2880 insertions(+), 30 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/unit/modules/test_rebootmgr.py
|
|
create mode 100644 tests/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 9fea7af07f..f6780e1694 100644
|
|
--- a/doc/ref/modules/all/index.rst
|
|
+++ b/doc/ref/modules/all/index.rst
|
|
@@ -394,6 +394,7 @@ execution modules
|
|
rbac_solaris
|
|
rbenv
|
|
rdp
|
|
+ rebootmgr
|
|
redismod
|
|
reg
|
|
rest_pkg
|
|
@@ -480,6 +481,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/salt/executors/transactional_update.py b/salt/executors/transactional_update.py
|
|
new file mode 100644
|
|
index 0000000000..ef7d92bc05
|
|
--- /dev/null
|
|
+++ b/salt/executors/transactional_update.py
|
|
@@ -0,0 +1,126 @@
|
|
+"""
|
|
+Transactional executor module
|
|
+
|
|
+.. versionadded:: TBD
|
|
+
|
|
+"""
|
|
+
|
|
+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]
|
|
+
|
|
+ """
|
|
+ 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:
|
|
+ result = __executors__["direct_call.execute"](
|
|
+ opts, data, __salt__[DELEGATION_MAP[fun]], args, kwargs
|
|
+ )
|
|
+ elif module in delegated_modules or fun in delegated_functions:
|
|
+ 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..d25faac3b7 100644
|
|
--- a/salt/grains/extra.py
|
|
+++ b/salt/grains/extra.py
|
|
@@ -1,16 +1,11 @@
|
|
-# -*- coding: utf-8 -*-
|
|
-
|
|
-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
|
|
|
|
@@ -70,7 +65,32 @@ def config():
|
|
|
|
def suse_backported_capabilities():
|
|
return {
|
|
- '__suse_reserved_pkg_all_versions_support': True,
|
|
- '__suse_reserved_pkg_patches_support': True,
|
|
- '__suse_reserved_saltutil_states_support': True
|
|
+ "__suse_reserved_pkg_all_versions_support": True,
|
|
+ "__suse_reserved_pkg_patches_support": True,
|
|
+ "__suse_reserved_saltutil_states_support": True,
|
|
}
|
|
+
|
|
+
|
|
+def __secure_boot():
|
|
+ """Detect if secure-boot is enabled."""
|
|
+ enabled = False
|
|
+ sboot = glob.glob("/sys/firmware/efi/vars/SecureBoot-*/data")
|
|
+ if len(sboot) == 1:
|
|
+ with salt.utils.files.fopen(sboot[0], "rb") as fd:
|
|
+ enabled = fd.read()[-1:] == b"\x01"
|
|
+ return enabled
|
|
+
|
|
+
|
|
+def uefi():
|
|
+ """Populate UEFI grains."""
|
|
+ grains = {
|
|
+ "efi": os.path.exists("/sys/firmware/efi/systab"),
|
|
+ "efi-secure-boot": __secure_boot(),
|
|
+ }
|
|
+
|
|
+ return grains
|
|
+
|
|
+
|
|
+def transactional():
|
|
+ """Determine if the system in transactional."""
|
|
+ return {"transactional": bool(salt.utils.path.which("transactional-update"))}
|
|
diff --git a/salt/modules/chroot.py b/salt/modules/chroot.py
|
|
index 6512a70f88..1e2948607e 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
|
|
@@ -21,6 +18,7 @@ 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"}
|
|
|
|
@@ -79,6 +77,38 @@ def create(root):
|
|
return True
|
|
|
|
|
|
+def in_chroot():
|
|
+ """
|
|
+ Return True if the process is inside a chroot jail
|
|
+
|
|
+ .. versionadded:: TBD
|
|
+
|
|
+ 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 +146,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)
|
|
@@ -194,7 +224,7 @@ 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)
|
|
@@ -206,7 +236,7 @@ def _create_and_execute_salt_state(root, chunks, file_refs, test, 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)
|
|
@@ -270,12 +300,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)
|
|
diff --git a/salt/modules/rebootmgr.py b/salt/modules/rebootmgr.py
|
|
new file mode 100644
|
|
index 0000000000..96133c754b
|
|
--- /dev/null
|
|
+++ b/salt/modules/rebootmgr.py
|
|
@@ -0,0 +1,357 @@
|
|
+"""
|
|
+:maintainer: Alberto Planas <aplanas@suse.com>
|
|
+:maturity: new
|
|
+:depends: None
|
|
+:platform: Linux
|
|
+"""
|
|
+
|
|
+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 03e7268cd4..49e5bd813f 100644
|
|
--- a/salt/modules/systemd_service.py
|
|
+++ b/salt/modules/systemd_service.py
|
|
@@ -64,7 +64,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,
|
|
@@ -1447,3 +1450,19 @@ def firstboot(
|
|
raise CommandExecutionError("systemd-firstboot error: {}".format(out["stderr"]))
|
|
|
|
return True
|
|
+
|
|
+
|
|
+def offline():
|
|
+ """
|
|
+ .. versionadded:: TBD
|
|
+
|
|
+ Check if systemd is working in offline mode, where is not possible
|
|
+ to talk with PID 1.
|
|
+
|
|
+ CLI Example:
|
|
+
|
|
+ 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..9b14557e07
|
|
--- /dev/null
|
|
+++ b/salt/modules/transactional_update.py
|
|
@@ -0,0 +1,1270 @@
|
|
+"""Transactional update
|
|
+====================
|
|
+
|
|
+.. versionadded: TBD
|
|
+
|
|
+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 present 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, loosing 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 --continute --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
|
|
+
|
|
+import salt.client.ssh.state
|
|
+import salt.client.ssh.wrapper.state
|
|
+import salt.exceptions
|
|
+import salt.utils.args
|
|
+
|
|
+__func_alias__ = {"apply_": "apply"}
|
|
+
|
|
+log = logging.getLogger(__name__)
|
|
+
|
|
+
|
|
+def __virtual__():
|
|
+ """
|
|
+ transactional-update command is required.
|
|
+ """
|
|
+ if __utils__["path.which"]("transactional-update"):
|
|
+ return True
|
|
+ else:
|
|
+ return (False, "Module transactional_update requires a transactional system")
|
|
+
|
|
+
|
|
+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 (KeyError, ValueError):
|
|
+ return {"result": False, "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__
|
|
+ )
|
|
+ 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, **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)
|
|
+
|
|
+ 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
|
|
+
|
|
+ """
|
|
+ # Get a copy of the pillar data, to avoid overwriting the current
|
|
+ # pillar, instead the one delegated
|
|
+ pillar = copy.deepcopy(__pillar__)
|
|
+ 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.SSHHighState(
|
|
+ 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, **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)
|
|
+
|
|
+ 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
|
|
+
|
|
+ """
|
|
+ # Get a copy of the pillar data, to avoid overwriting the current
|
|
+ # pillar, instead the one delegated
|
|
+ pillar = copy.deepcopy(__pillar__)
|
|
+ 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.SSHHighState(
|
|
+ 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, **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)
|
|
+
|
|
+ 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
|
|
+
|
|
+ """
|
|
+ # Get a copy of the pillar data, to avoid overwriting the current
|
|
+ # pillar, instead the one delegated
|
|
+ pillar = copy.deepcopy(__pillar__)
|
|
+ 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/utils/systemd.py b/salt/utils/systemd.py
|
|
index e830d36ed4..4d902bc920 100644
|
|
--- a/salt/utils/systemd.py
|
|
+++ b/salt/utils/systemd.py
|
|
@@ -1,18 +1,14 @@
|
|
-# -*- coding: utf-8 -*-
|
|
"""
|
|
Contains systemd related help files
|
|
"""
|
|
-# import python libs
|
|
-from __future__ import absolute_import, print_function, unicode_literals
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
+import salt.utils.path
|
|
import salt.utils.stringutils
|
|
-
|
|
-# Import Salt libs
|
|
from salt.exceptions import SaltInvocationError
|
|
|
|
log = logging.getLogger(__name__)
|
|
@@ -49,6 +45,27 @@ def booted(context=None):
|
|
return ret
|
|
|
|
|
|
+def offline(context=None):
|
|
+ """Return True is systemd is in offline mode"""
|
|
+ contextkey = "salt.utils.systemd.offline"
|
|
+ if isinstance(context, dict):
|
|
+ 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/unit/modules/test_chroot.py b/tests/unit/modules/test_chroot.py
|
|
index 76811df46e..196e3ad27f 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.modules.chroot as chroot
|
|
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
|
|
@@ -75,6 +71,18 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin):
|
|
self.assertTrue(chroot.create("/chroot"))
|
|
makedirs.assert_called()
|
|
|
|
+ @patch("salt.modules.chroot.exist")
|
|
+ @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):
|
|
"""
|
|
diff --git a/tests/unit/modules/test_rebootmgr.py b/tests/unit/modules/test_rebootmgr.py
|
|
new file mode 100644
|
|
index 0000000000..a84dec2c1c
|
|
--- /dev/null
|
|
+++ b/tests/unit/modules/test_rebootmgr.py
|
|
@@ -0,0 +1,302 @@
|
|
+import pytest
|
|
+import salt.modules.rebootmgr as rebootmgr
|
|
+from salt.exceptions import CommandExecutionError
|
|
+from tests.support.mixins import LoaderModuleMockMixin
|
|
+from tests.support.mock import MagicMock, patch
|
|
+from tests.support.unit import TestCase
|
|
+
|
|
+
|
|
+class RebootMgrTestCase(TestCase, LoaderModuleMockMixin):
|
|
+ """
|
|
+ Test cases for salt.modules.rebootmgr
|
|
+ """
|
|
+
|
|
+ def setup_loader_modules(self):
|
|
+ return {rebootmgr: {"__salt__": {}, "__utils__": {}}}
|
|
+
|
|
+ def test_version(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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(self):
|
|
+ """
|
|
+ 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/unit/modules/test_transactional_update.py b/tests/unit/modules/test_transactional_update.py
|
|
new file mode 100644
|
|
index 0000000000..08a704c212
|
|
--- /dev/null
|
|
+++ b/tests/unit/modules/test_transactional_update.py
|
|
@@ -0,0 +1,681 @@
|
|
+import sys
|
|
+
|
|
+import pytest
|
|
+import salt.modules.transactional_update as tu
|
|
+import salt.utils.platform
|
|
+from salt.exceptions import CommandExecutionError
|
|
+from tests.support.mixins import LoaderModuleMockMixin
|
|
+from tests.support.mock import MagicMock, patch
|
|
+from tests.support.unit import TestCase, skipIf
|
|
+
|
|
+
|
|
+@skipIf(salt.utils.platform.is_windows(), "Do not run these tests on Windows")
|
|
+class TransactionalUpdateTestCase(TestCase, LoaderModuleMockMixin):
|
|
+ """
|
|
+ Test cases for salt.modules.transactional_update
|
|
+ """
|
|
+
|
|
+ def setup_loader_modules(self):
|
|
+ return {tu: {"__salt__": {}, "__utils__": {}}}
|
|
+
|
|
+ def test__global_params_no_self_update(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """Test transactional_update._pkg_params with single package"""
|
|
+ assert tu._pkg_params(pkg="pkg1", pkgs=None, args=None) == ["pkg1"]
|
|
+
|
|
+ def test__pkg_params_pkgs(self):
|
|
+ """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(self):
|
|
+ """Test transactional_update._pkg_params with packages"""
|
|
+ assert tu._pkg_params(pkg="pkg1", pkgs="pkg2", args=None) == [
|
|
+ "pkg1",
|
|
+ "pkg2",
|
|
+ ]
|
|
+
|
|
+ def test__pkg_params_args(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """Test transactional_update.run with missing command"""
|
|
+ with pytest.raises(CommandExecutionError):
|
|
+ tu.run(None)
|
|
+
|
|
+ def test_run_string(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """Test transactional_update.rollback with wrong snapshot"""
|
|
+ with pytest.raises(CommandExecutionError):
|
|
+ tu.rollback("error")
|
|
+
|
|
+ def test_rollback_default(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """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(self):
|
|
+ """Test transactional_update.call missing function name"""
|
|
+ with pytest.raises(CommandExecutionError):
|
|
+ tu.call("")
|
|
+
|
|
+ @patch("tempfile.mkdtemp")
|
|
+ def test_call_fails_untar(self, mkdtemp):
|
|
+ """Test transactional_update.call when tar fails"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ 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")
|
|
+ def test_call_fails_salt_thin(self, mkdtemp):
|
|
+ """Test transactional_update.chroot when fails salt_thin"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ 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, "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")
|
|
+ def test_call_fails_function(self, mkdtemp):
|
|
+ """Test transactional_update.chroot when fails the function"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ 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, "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")
|
|
+ def test_call_success_no_reboot(self, mkdtemp):
|
|
+ """Test transactional_update.chroot when succeed"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ 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("salt.modules.transactional_update.reboot")
|
|
+ @patch("salt.modules.transactional_update.pending_transaction")
|
|
+ @patch("tempfile.mkdtemp")
|
|
+ def test_call_success_reboot(self, mkdtemp, pending_transaction, reboot):
|
|
+ """Test transactional_update.chroot when succeed and reboot"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ pending_transaction.return_value = True
|
|
+ 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("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.assert_called_once()
|
|
+ reboot.assert_called_once()
|
|
+
|
|
+ @patch("tempfile.mkdtemp")
|
|
+ def test_call_success_parameters(self, mkdtemp):
|
|
+ """Test transactional_update.chroot when succeed with parameters"""
|
|
+ mkdtemp.return_value = "/var/cache/salt/minion/tmp01"
|
|
+ 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()
|
|
+
|
|
+ @patch("salt.modules.transactional_update._create_and_execute_salt_state")
|
|
+ @patch("salt.client.ssh.state.SSHHighState")
|
|
+ @patch("salt.fileclient.get_file_client")
|
|
+ @patch("salt.utils.state.get_sls_opts")
|
|
+ def test_sls(
|
|
+ self,
|
|
+ get_sls_opts,
|
|
+ get_file_client,
|
|
+ SSHHighState,
|
|
+ _create_and_execute_salt_state,
|
|
+ ):
|
|
+ """Test transactional_update.sls"""
|
|
+ SSHHighState.return_value = SSHHighState
|
|
+ SSHHighState.render_highstate.return_value = (None, [])
|
|
+ SSHHighState.state.reconcile_extend.return_value = (None, [])
|
|
+ SSHHighState.state.requisite_in.return_value = (None, [])
|
|
+ SSHHighState.state.verify_high.return_value = []
|
|
+
|
|
+ _create_and_execute_salt_state.return_value = "result"
|
|
+ opts_mock = {
|
|
+ "hash_type": "md5",
|
|
+ }
|
|
+ get_sls_opts.return_value = opts_mock
|
|
+ with patch.dict(tu.__opts__, opts_mock):
|
|
+ assert tu.sls("module") == "result"
|
|
+ _create_and_execute_salt_state.assert_called_once()
|
|
+
|
|
+ @patch("salt.modules.transactional_update._create_and_execute_salt_state")
|
|
+ @patch("salt.client.ssh.state.SSHHighState")
|
|
+ @patch("salt.fileclient.get_file_client")
|
|
+ @patch("salt.utils.state.get_sls_opts")
|
|
+ def test_highstate(
|
|
+ self,
|
|
+ get_sls_opts,
|
|
+ get_file_client,
|
|
+ SSHHighState,
|
|
+ _create_and_execute_salt_state,
|
|
+ ):
|
|
+ """Test transactional_update.highstage"""
|
|
+ SSHHighState.return_value = SSHHighState
|
|
+
|
|
+ _create_and_execute_salt_state.return_value = "result"
|
|
+ opts_mock = {
|
|
+ "hash_type": "md5",
|
|
+ }
|
|
+ get_sls_opts.return_value = opts_mock
|
|
+ with patch.dict(tu.__opts__, opts_mock):
|
|
+ assert tu.highstate() == "result"
|
|
+ _create_and_execute_salt_state.assert_called_once()
|
|
+
|
|
+ @patch("salt.modules.transactional_update._create_and_execute_salt_state")
|
|
+ @patch("salt.client.ssh.state.SSHState")
|
|
+ @patch("salt.utils.state.get_sls_opts")
|
|
+ def test_single(self, get_sls_opts, SSHState, _create_and_execute_salt_state):
|
|
+ """Test transactional_update.single"""
|
|
+ SSHState.return_value = SSHState
|
|
+ SSHState.verify_data.return_value = None
|
|
+
|
|
+ _create_and_execute_salt_state.return_value = "result"
|
|
+ opts_mock = {
|
|
+ "hash_type": "md5",
|
|
+ }
|
|
+ get_sls_opts.return_value = opts_mock
|
|
+ with patch.dict(tu.__opts__, opts_mock):
|
|
+ assert tu.single("pkg.installed", name="emacs") == "result"
|
|
+ _create_and_execute_salt_state.assert_called_once()
|
|
--
|
|
2.29.2
|
|
|
|
|