diff --git a/_lastrevision b/_lastrevision index 1f5953f..621dcf9 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -24bd64b440c2c3f0f154a1b7f7216de20dc07df8 \ No newline at end of file +1a73678e768b896323b9d2d1f903a400e48e51e1 \ No newline at end of file diff --git a/add-migrated-state-and-gpg-key-management-functions-.patch b/add-migrated-state-and-gpg-key-management-functions-.patch new file mode 100644 index 0000000..cbcc578 --- /dev/null +++ b/add-migrated-state-and-gpg-key-management-functions-.patch @@ -0,0 +1,1556 @@ +From 5254ec34316a0924edb4856f84e6092fafe479fa Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Tue, 20 Oct 2020 11:43:09 +0200 +Subject: [PATCH] Add "migrated" state and GPG key management functions + (#290) + +* rpm_lowpkg: add API for GPG keys + +* zypperpkg: do not quote the repo name + +* pkgrepo: add migrated function + +* pkg: unify apt and rpm API for key repo + +aptpkg is the virtual package "pkg" for Debian, and contains some API +for key management. + +This patch add a similar API for zypperpkg and yumpkg, also part of the +same virtual package, based on the counterpart from rpm_lowpkg API. +--- + changelog/58782.added | 1 + + salt/modules/aptpkg.py | 7 +- + salt/modules/rpm_lowpkg.py | 151 ++++++++ + salt/modules/yumpkg.py | 88 +++++ + salt/modules/zypperpkg.py | 90 ++++- + salt/states/pkgrepo.py | 208 ++++++++++ + tests/unit/modules/test_rpm_lowpkg.py | 215 +++++++++++ + tests/unit/modules/test_yumpkg.py | 43 ++- + tests/unit/modules/test_zypperpkg.py | 40 +- + tests/unit/states/test_pkgrepo.py | 527 ++++++++++++++++++++++++++ + 10 files changed, 1363 insertions(+), 7 deletions(-) + create mode 100644 changelog/58782.added + create mode 100644 tests/unit/states/test_pkgrepo.py + +diff --git a/changelog/58782.added b/changelog/58782.added +new file mode 100644 +index 0000000000..f9e69f64f2 +--- /dev/null ++++ b/changelog/58782.added +@@ -0,0 +1 @@ ++Add GPG key functions in "lowpkg" and a "migrated" function in the "pkgrepo" state for repository and GPG key migration. +\ No newline at end of file +diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py +index 765d69aff2..28b8597ef5 100644 +--- a/salt/modules/aptpkg.py ++++ b/salt/modules/aptpkg.py +@@ -1878,7 +1878,7 @@ def _convert_if_int(value): + return value + + +-def get_repo_keys(): ++def get_repo_keys(**kwargs): + ''' + .. versionadded:: 2017.7.0 + +@@ -1950,7 +1950,9 @@ def get_repo_keys(): + return ret + + +-def add_repo_key(path=None, text=None, keyserver=None, keyid=None, saltenv='base'): ++def add_repo_key( ++ path=None, text=None, keyserver=None, keyid=None, saltenv='base', **kwargs ++): + ''' + .. versionadded:: 2017.7.0 + +@@ -1976,7 +1978,6 @@ def add_repo_key(path=None, text=None, keyserver=None, keyid=None, saltenv='base + salt '*' pkg.add_repo_key keyserver='keyserver.example' keyid='0000AAAA' + ''' + cmd = ['apt-key'] +- kwargs = {} + + current_repo_keys = get_repo_keys() + +diff --git a/salt/modules/rpm_lowpkg.py b/salt/modules/rpm_lowpkg.py +index c8a87276b2..fee0221a7c 100644 +--- a/salt/modules/rpm_lowpkg.py ++++ b/salt/modules/rpm_lowpkg.py +@@ -823,3 +823,154 @@ def checksum(*paths, **kwargs): + python_shell=False)) + + return ret ++ ++ ++def list_gpg_keys(info=False, root=None): ++ """Return the list of all the GPG keys stored in the RPM database ++ ++ .. versionadded:: TBD ++ ++ info ++ get the key information, returing a dictionary instead of a ++ list ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' lowpkg.list_gpg_keys ++ salt '*' lowpkg.list_gpg_keys info=True ++ ++ """ ++ cmd = ["rpm"] ++ if root: ++ cmd.extend(["--root", root]) ++ cmd.extend(["-qa", "gpg-pubkey*"]) ++ keys = __salt__["cmd.run_stdout"](cmd, python_shell=False).splitlines() ++ if info: ++ return {key: info_gpg_key(key, root=root) for key in keys} ++ else: ++ return keys ++ ++ ++def info_gpg_key(key, root=None): ++ """Return a dictionary with the information of a GPG key parsed ++ ++ .. versionadded:: TBD ++ ++ key ++ key identificatior ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' lowpkg.info_gpg_key gpg-pubkey-3dbdc284-53674dd4 ++ ++ """ ++ cmd = ["rpm"] ++ if root: ++ cmd.extend(["--root", root]) ++ cmd.extend(["-qi", key]) ++ info = __salt__["cmd.run_stdout"](cmd, python_shell=False) ++ ++ res = {} ++ # The parser algorithm is very ad-hoc. Works under the ++ # expectation that all the fields are of the type "key: value" in ++ # a single line, except "Description", that will be composed of ++ # multiple lines. Note that even if the official `rpm` makes this ++ # field the last one, other (like openSUSE) exted it with more ++ # fields. ++ in_description = False ++ description = [] ++ for line in info.splitlines(): ++ if line.startswith("Description"): ++ in_description = True ++ elif in_description: ++ description.append(line) ++ if line.startswith("-----END"): ++ res["Description"] = "\n".join(description) ++ in_description = False ++ elif line: ++ key, _, value = line.partition(":") ++ value = value.strip() ++ if "Date" in key: ++ try: ++ value = datetime.datetime.strptime( ++ value, "%a %d %b %Y %H:%M:%S %p %Z" ++ ) ++ except ValueError: ++ pass ++ elif "Size" in key: ++ try: ++ value = int(value) ++ except TypeError: ++ pass ++ elif "(none)" in value: ++ value = None ++ res[key.strip()] = value ++ return res ++ ++ ++def import_gpg_key(key, root=None): ++ """Import a new key into the key storage ++ ++ .. versionadded:: TBD ++ ++ key ++ public key block content ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' lowpkg.import_gpg_key "-----BEGIN ..." ++ ++ """ ++ key_file = salt.utils.files.mkstemp() ++ with salt.utils.files.fopen(key_file, "w") as f: ++ f.write(key) ++ ++ cmd = ["rpm"] ++ if root: ++ cmd.extend(["--root", root]) ++ cmd.extend(["--import", key_file]) ++ ret = __salt__["cmd.retcode"](cmd) ++ ++ os.remove(key_file) ++ ++ return ret == 0 ++ ++ ++def remove_gpg_key(key, root=None): ++ """Remove a key from the key storage ++ ++ .. versionadded:: TBD ++ ++ key ++ key identificatior ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' lowpkg.remove_gpg_key gpg-pubkey-3dbdc284-53674dd4 ++ ++ """ ++ cmd = ["rpm"] ++ if root: ++ cmd.extend(["--root", root]) ++ cmd.extend(["-e", key]) ++ return __salt__["cmd.retcode"](cmd) == 0 +diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py +index 04ab240cd4..85a2dbd857 100644 +--- a/salt/modules/yumpkg.py ++++ b/salt/modules/yumpkg.py +@@ -3271,3 +3271,91 @@ def list_installed_patches(**kwargs): + salt '*' pkg.list_installed_patches + ''' + return _get_patches(installed_only=True) ++ ++ ++def get_repo_keys(info=False, root=None, **kwargs): ++ """Return the list of all the GPG keys stored in the RPM database ++ ++ .. versionadded:: TBD ++ ++ info ++ get the key information, returing a dictionary instead of a ++ list ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.get_repo_keys ++ salt '*' pkg.get_repo_keys info=True ++ ++ """ ++ return __salt__["lowpkg.list_gpg_keys"](info, root) ++ ++ ++def add_repo_key(path=None, text=None, root=None, saltenv="base", **kwargs): ++ """Import a new key into the key storage ++ ++ .. versionadded:: TBD ++ ++ path ++ the path of the key file to import ++ ++ text ++ the key data to import, in string form ++ ++ root ++ use root as top level directory (default: "/") ++ ++ saltenv ++ the environment the key file resides in ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.add_repo_key 'salt://apt/sources/test.key' ++ salt '*' pkg.add_repo_key text="'$KEY1'" ++ ++ """ ++ if not path and not text: ++ raise SaltInvocationError("Provide a key to add") ++ ++ if path and text: ++ raise SaltInvocationError("Add a key via path or key") ++ ++ if path: ++ cache_path = __salt__["cp.cache_file"](path, saltenv) ++ ++ if not cache_path: ++ log.error("Unable to get cached copy of file: %s", path) ++ return False ++ ++ with salt.utils.files.fopen(cache_path, "r") as f: ++ text = f.read() ++ ++ return __salt__["lowpkg.import_gpg_key"](text, root) ++ ++ ++def del_repo_key(keyid, root=None, **kwargs): ++ """Remove a key from the key storage ++ ++ .. versionadded:: TBD ++ ++ keyid ++ key identificatior ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.del_repo_key keyid=gpg-pubkey-3dbdc284-53674dd4 ++ ++ """ ++ return __salt__["lowpkg.remove_gpg_key"](keyid, root) +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index d84a6af6e0..fab7736701 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -1270,7 +1270,7 @@ def mod_repo(repo, **kwargs): + cmd_opt.append("--priority={0}".format(kwargs.get('priority', DEFAULT_PRIORITY))) + + if 'humanname' in kwargs: +- cmd_opt.append("--name='{0}'".format(kwargs.get('humanname'))) ++ cmd_opt.extend(["--name", kwargs.get("humanname")]) + + if kwargs.get('gpgautoimport') is True: + global_cmd_opt.append('--gpg-auto-import-keys') +@@ -2879,3 +2879,91 @@ def resolve_capabilities(pkgs, refresh=False, root=None, **kwargs): + else: + ret.append(name) + return ret ++ ++ ++def get_repo_keys(info=False, root=None, **kwargs): ++ """Return the list of all the GPG keys stored in the RPM database ++ ++ .. versionadded:: TBD ++ ++ info ++ get the key information, returing a dictionary instead of a ++ list ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.get_repo_keys ++ salt '*' pkg.get_repo_keys info=True ++ ++ """ ++ return __salt__["lowpkg.list_gpg_keys"](info, root) ++ ++ ++def add_repo_key(path=None, text=None, root=None, saltenv="base", **kwargs): ++ """Import a new key into the key storage ++ ++ .. versionadded:: TBD ++ ++ path ++ the path of the key file to import ++ ++ text ++ the key data to import, in string form ++ ++ root ++ use root as top level directory (default: "/") ++ ++ saltenv ++ the environment the key file resides in ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.add_repo_key 'salt://apt/sources/test.key' ++ salt '*' pkg.add_repo_key text="'$KEY1'" ++ ++ """ ++ if not path and not text: ++ raise SaltInvocationError("Provide a key to add") ++ ++ if path and text: ++ raise SaltInvocationError("Add a key via path or key") ++ ++ if path: ++ cache_path = __salt__["cp.cache_file"](path, saltenv) ++ ++ if not cache_path: ++ log.error("Unable to get cached copy of file: %s", path) ++ return False ++ ++ with salt.utils.files.fopen(cache_path, "r") as f: ++ text = f.read() ++ ++ return __salt__["lowpkg.import_gpg_key"](text, root) ++ ++ ++def del_repo_key(keyid, root=None, **kwargs): ++ """Remove a key from the key storage ++ ++ .. versionadded:: TBD ++ ++ keyid ++ key identificatior ++ ++ root ++ use root as top level directory (default: "/") ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.del_repo_key keyid=gpg-pubkey-3dbdc284-53674dd4 ++ ++ """ ++ return __salt__["lowpkg.remove_gpg_key"](keyid, root) +diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py +index c39e857580..6c42d17d32 100644 +--- a/salt/states/pkgrepo.py ++++ b/salt/states/pkgrepo.py +@@ -84,6 +84,7 @@ package managers are APT, DNF, YUM and Zypper. Here is some example SLS: + + # Import Python libs + from __future__ import absolute_import, print_function, unicode_literals ++import os + import sys + + # Import salt libs +@@ -96,6 +97,7 @@ import salt.utils.pkg.rpm + + # Import 3rd-party libs + from salt.ext import six ++import salt.utils.versions + + + def __virtual__(): +@@ -643,3 +645,209 @@ def absent(name, **kwargs): + ret['comment'] = 'Failed to remove repo {0}'.format(name) + + return ret ++ ++ ++def _normalize_repo(repo): ++ """Normalize the get_repo information""" ++ # `pkg.get_repo()` specific virtual module implementation is ++ # parsing the information directly from the repository ++ # configuration file, and can be different from the ones that ++ # `pkg.mod_repo()` accepts ++ ++ # If the field is not present will be dropped ++ suse = { ++ # "alias": "repo", ++ "name": "humanname", ++ "priority": "priority", ++ "enabled": "enabled", ++ "autorefresh": "refresh", ++ "gpgcheck": "gpgcheck", ++ "keepackages": "cache", ++ "baseurl": "url", ++ } ++ translator = { ++ "Suse": suse, ++ } ++ table = translator.get(__grains__["os_family"], {}) ++ return {table[k]: v for k, v in repo.items() if k in table} ++ ++ ++def _normalize_key(key): ++ """Normalize the info_gpg_key information""" ++ ++ # If the field is not present will be dropped ++ rpm = { ++ "Description": "key", ++ } ++ translator = { ++ "Suse": rpm, ++ "RedHat": rpm, ++ } ++ table = translator.get(__grains__["os_family"], {}) ++ return {table[k]: v for k, v in key.items() if k in table} ++ ++ ++def _repos_keys_migrate_drop(root, keys, drop): ++ """Helper function to calculate repost and key migrations""" ++ ++ def _d2s(d): ++ """Serialize a dict and store in a set""" ++ return { ++ (k, tuple((_k, _v) for _k, _v in sorted(v.items()))) ++ for k, v in sorted(d.items()) ++ } ++ ++ src_repos = _d2s( ++ {k: _normalize_repo(v) for k, v in __salt__["pkg.list_repos"]().items()} ++ ) ++ # There is no guarantee that the target repository is even initialized ++ try: ++ tgt_repos = _d2s( ++ { ++ k: _normalize_repo(v) ++ for k, v in __salt__["pkg.list_repos"](root=root).items() ++ } ++ ) ++ except Exception: # pylint: disable=broad-except ++ tgt_repos = set() ++ ++ src_keys = set() ++ tgt_keys = set() ++ if keys: ++ src_keys = _d2s( ++ { ++ k: _normalize_key(v) ++ for k, v in __salt__["lowpkg.list_gpg_keys"](info=True).items() ++ } ++ ) ++ try: ++ tgt_keys = _d2s( ++ { ++ k: _normalize_key(v) ++ for k, v in __salt__["lowpkg.list_gpg_keys"]( ++ info=True, root=root ++ ).items() ++ } ++ ) ++ except Exception: # pylint: disable=broad-except ++ pass ++ ++ repos_to_migrate = src_repos - tgt_repos ++ repos_to_drop = tgt_repos - src_repos if drop else set() ++ ++ keys_to_migrate = src_keys - tgt_keys ++ keys_to_drop = tgt_keys - src_keys if drop else set() ++ ++ return (repos_to_migrate, repos_to_drop, keys_to_migrate, keys_to_drop) ++ ++ ++def _copy_repository_to(root): ++ repo = { ++ "Suse": ["/etc/zypp/repos.d"], ++ "RedHat": ["/etc/yum.conf", "/etc/yum.repos.d"], ++ } ++ for src in repo.get(__grains__["os_family"], []): ++ dst = os.path.join(root, os.path.relpath(src, os.path.sep)) ++ __salt__["file.copy"](src=src, dst=dst, recurse=True) ++ ++ ++def migrated(name, keys=True, drop=False, method=None, **kwargs): ++ """Migrate a repository from one directory to another, including the ++ GPG keys if requested ++ ++ .. versionadded:: TBD ++ ++ name ++ Directory were to migrate the repositories. For example, if we ++ are booting from a USB key and we mounted the rootfs in ++ "/mnt", the repositories will live in "/mnt/etc/yum.repos.d" ++ or in "/etc/zypp/repos.d", depending on the system. For both ++ cases the expected value for "name" would be "/mnt" ++ ++ keys ++ If is is True, will migrate all the keys ++ ++ drop ++ If True, the target repositories that do not exist in the ++ source will be dropped ++ ++ method ++ If None or "salt", it will use the Salt API to migrate the ++ repositories, if "copy", it will copy the repository files ++ directly ++ ++ """ ++ ret = {"name": name, "result": False, "changes": {}, "comment": ""} ++ ++ if __grains__["os_family"] not in ("Suse",): ++ ret["comment"] = "Migration not supported for this platform" ++ return ret ++ ++ if keys and "lowpkg.import_gpg_key" not in __salt__: ++ ret["comment"] = "Keys cannot be migrated for this platform" ++ return ret ++ ++ if method not in (None, "salt", "copy"): ++ ret["comment"] = "Migration method not supported" ++ return ret ++ ++ ( ++ repos_to_migrate, ++ repos_to_drop, ++ keys_to_migrate, ++ keys_to_drop, ++ ) = _repos_keys_migrate_drop(name, keys, drop) ++ ++ if not any((repos_to_migrate, repos_to_drop, keys_to_migrate, keys_to_drop)): ++ ret["result"] = True ++ ret["comment"] = "Repositories are already migrated" ++ return ret ++ ++ if __opts__["test"]: ++ ret["result"] = None ++ ret["comment"] = "There are keys or repositories to migrate or drop" ++ ret["changes"] = { ++ "repos to migrate": [repo for repo, _ in repos_to_migrate], ++ "repos to drop": [repo for repo, _ in repos_to_drop], ++ "keys to migrate": [key for key, _ in keys_to_migrate], ++ "keys to drop": [key for key, _ in keys_to_drop], ++ } ++ return ret ++ ++ for repo, repo_info in repos_to_migrate: ++ if method == "copy": ++ _copy_repository_to(name) ++ else: ++ __salt__["pkg.mod_repo"](repo, **dict(repo_info), root=name) ++ for repo, _ in repos_to_drop: ++ __salt__["pkg.del_repo"](repo, root=name) ++ ++ for _, key_info in keys_to_migrate: ++ __salt__["lowpkg.import_gpg_key"](dict(key_info)["key"], root=name) ++ for key, _ in keys_to_drop: ++ __salt__["lowpkg.remove_gpg_key"](key, root=name) ++ ++ ( ++ rem_repos_to_migrate, ++ rem_repos_to_drop, ++ rem_keys_to_migrate, ++ rem_keys_to_drop, ++ ) = _repos_keys_migrate_drop(name, keys, drop) ++ ++ if any( ++ (rem_repos_to_migrate, rem_repos_to_drop, rem_keys_to_migrate, rem_keys_to_drop) ++ ): ++ ret["result"] = False ++ ret["comment"] = "Migration of repositories failed" ++ return ret ++ ++ ret["result"] = True ++ ret["comment"] = "Repositories synchronized" ++ ret["changes"] = { ++ "repos migrated": [repo for repo, _ in repos_to_migrate], ++ "repos dropped": [repo for repo, _ in repos_to_drop], ++ "keys migrated": [key for key, _ in keys_to_migrate], ++ "keys dropped": [key for key, _ in keys_to_drop], ++ } ++ ++ return ret +diff --git a/tests/unit/modules/test_rpm_lowpkg.py b/tests/unit/modules/test_rpm_lowpkg.py +index b6cbd9e5cb..ff3678fde5 100644 +--- a/tests/unit/modules/test_rpm_lowpkg.py ++++ b/tests/unit/modules/test_rpm_lowpkg.py +@@ -5,6 +5,7 @@ + + # Import Python Libs + from __future__ import absolute_import ++import datetime + + # Import Salt Testing Libs + from tests.support.mixins import LoaderModuleMockMixin +@@ -205,3 +206,217 @@ class RpmTestCase(TestCase, LoaderModuleMockMixin): + with patch('salt.modules.rpm_lowpkg.rpm.labelCompare', MagicMock(return_value=0)), \ + patch('salt.modules.rpm_lowpkg.HAS_RPM', False): + self.assertEqual(-1, rpm.version_cmp('1', '2')) # mock returns -1, a python implementation was called ++ ++ def test_list_gpg_keys_no_info(self): ++ """ ++ Test list_gpg_keys with no extra information ++ """ ++ mock = MagicMock(return_value="\n".join(["gpg-pubkey-1", "gpg-pubkey-2"])) ++ with patch.dict(rpm.__salt__, {"cmd.run_stdout": mock}): ++ self.assertEqual(rpm.list_gpg_keys(), ["gpg-pubkey-1", "gpg-pubkey-2"]) ++ self.assertFalse(_called_with_root(mock)) ++ ++ def test_list_gpg_keys_no_info_root(self): ++ """ ++ Test list_gpg_keys with no extra information and root ++ """ ++ mock = MagicMock(return_value="\n".join(["gpg-pubkey-1", "gpg-pubkey-2"])) ++ with patch.dict(rpm.__salt__, {"cmd.run_stdout": mock}): ++ self.assertEqual( ++ rpm.list_gpg_keys(root="/mnt"), ["gpg-pubkey-1", "gpg-pubkey-2"] ++ ) ++ self.assertTrue(_called_with_root(mock)) ++ ++ @patch("salt.modules.rpm_lowpkg.info_gpg_key") ++ def test_list_gpg_keys_info(self, info_gpg_key): ++ """ ++ Test list_gpg_keys with extra information ++ """ ++ info_gpg_key.side_effect = lambda x, root: { ++ "Description": "key for {}".format(x) ++ } ++ mock = MagicMock(return_value="\n".join(["gpg-pubkey-1", "gpg-pubkey-2"])) ++ with patch.dict(rpm.__salt__, {"cmd.run_stdout": mock}): ++ self.assertEqual( ++ rpm.list_gpg_keys(info=True), ++ { ++ "gpg-pubkey-1": {"Description": "key for gpg-pubkey-1"}, ++ "gpg-pubkey-2": {"Description": "key for gpg-pubkey-2"}, ++ }, ++ ) ++ self.assertFalse(_called_with_root(mock)) ++ ++ def test_info_gpg_key(self): ++ """ ++ Test info_gpg_keys from a normal output ++ """ ++ info = """Name : gpg-pubkey ++Version : 3dbdc284 ++Release : 53674dd4 ++Architecture: (none) ++Install Date: Fri 08 Mar 2019 11:57:44 AM UTC ++Group : Public Keys ++Size : 0 ++License : pubkey ++Signature : (none) ++Source RPM : (none) ++Build Date : Mon 05 May 2014 10:37:40 AM UTC ++Build Host : localhost ++Packager : openSUSE Project Signing Key ++Summary : gpg(openSUSE Project Signing Key ) ++Description : ++-----BEGIN PGP PUBLIC KEY BLOCK----- ++Version: rpm-4.14.2.1 (NSS-3) ++ ++mQENBEkUTD8BCADWLy5d5IpJedHQQSXkC1VK/oAZlJEeBVpSZjMCn8LiHaI9Wq3G ++3Vp6wvsP1b3kssJGzVFNctdXt5tjvOLxvrEfRJuGfqHTKILByqLzkeyWawbFNfSQ ++93/8OunfSTXC1Sx3hgsNXQuOrNVKrDAQUqT620/jj94xNIg09bLSxsjN6EeTvyiO ++mtE9H1J03o9tY6meNL/gcQhxBvwuo205np0JojYBP0pOfN8l9hnIOLkA0yu4ZXig ++oKOVmf4iTjX4NImIWldT+UaWTO18NWcCrujtgHueytwYLBNV5N0oJIP2VYuLZfSD ++VYuPllv7c6O2UEOXJsdbQaVuzU1HLocDyipnABEBAAG0NG9wZW5TVVNFIFByb2pl ++Y3QgU2lnbmluZyBLZXkgPG9wZW5zdXNlQG9wZW5zdXNlLm9yZz6JATwEEwECACYC ++GwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCU2dN1AUJHR8ElQAKCRC4iy/UPb3C ++hGQrB/9teCZ3Nt8vHE0SC5NmYMAE1Spcjkzx6M4r4C70AVTMEQh/8BvgmwkKP/qI ++CWo2vC1hMXRgLg/TnTtFDq7kW+mHsCXmf5OLh2qOWCKi55Vitlf6bmH7n+h34Sha ++Ei8gAObSpZSF8BzPGl6v0QmEaGKM3O1oUbbB3Z8i6w21CTg7dbU5vGR8Yhi9rNtr ++hqrPS+q2yftjNbsODagaOUb85ESfQGx/LqoMePD+7MqGpAXjKMZqsEDP0TbxTwSk ++4UKnF4zFCYHPLK3y/hSH5SEJwwPY11l6JGdC1Ue8Zzaj7f//axUs/hTC0UZaEE+a ++5v4gbqOcigKaFs9Lc3Bj8b/lE10Y ++=i2TA ++-----END PGP PUBLIC KEY BLOCK----- ++ ++""" ++ mock = MagicMock(return_value=info) ++ with patch.dict(rpm.__salt__, {"cmd.run_stdout": mock}): ++ self.assertEqual( ++ rpm.info_gpg_key("key"), ++ { ++ "Name": "gpg-pubkey", ++ "Version": "3dbdc284", ++ "Release": "53674dd4", ++ "Architecture": None, ++ "Install Date": datetime.datetime(2019, 3, 8, 11, 57, 44), ++ "Group": "Public Keys", ++ "Size": 0, ++ "License": "pubkey", ++ "Signature": None, ++ "Source RPM": None, ++ "Build Date": datetime.datetime(2014, 5, 5, 10, 37, 40), ++ "Build Host": "localhost", ++ "Packager": "openSUSE Project Signing Key ", ++ "Summary": "gpg(openSUSE Project Signing Key )", ++ "Description": """-----BEGIN PGP PUBLIC KEY BLOCK----- ++Version: rpm-4.14.2.1 (NSS-3) ++ ++mQENBEkUTD8BCADWLy5d5IpJedHQQSXkC1VK/oAZlJEeBVpSZjMCn8LiHaI9Wq3G ++3Vp6wvsP1b3kssJGzVFNctdXt5tjvOLxvrEfRJuGfqHTKILByqLzkeyWawbFNfSQ ++93/8OunfSTXC1Sx3hgsNXQuOrNVKrDAQUqT620/jj94xNIg09bLSxsjN6EeTvyiO ++mtE9H1J03o9tY6meNL/gcQhxBvwuo205np0JojYBP0pOfN8l9hnIOLkA0yu4ZXig ++oKOVmf4iTjX4NImIWldT+UaWTO18NWcCrujtgHueytwYLBNV5N0oJIP2VYuLZfSD ++VYuPllv7c6O2UEOXJsdbQaVuzU1HLocDyipnABEBAAG0NG9wZW5TVVNFIFByb2pl ++Y3QgU2lnbmluZyBLZXkgPG9wZW5zdXNlQG9wZW5zdXNlLm9yZz6JATwEEwECACYC ++GwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCU2dN1AUJHR8ElQAKCRC4iy/UPb3C ++hGQrB/9teCZ3Nt8vHE0SC5NmYMAE1Spcjkzx6M4r4C70AVTMEQh/8BvgmwkKP/qI ++CWo2vC1hMXRgLg/TnTtFDq7kW+mHsCXmf5OLh2qOWCKi55Vitlf6bmH7n+h34Sha ++Ei8gAObSpZSF8BzPGl6v0QmEaGKM3O1oUbbB3Z8i6w21CTg7dbU5vGR8Yhi9rNtr ++hqrPS+q2yftjNbsODagaOUb85ESfQGx/LqoMePD+7MqGpAXjKMZqsEDP0TbxTwSk ++4UKnF4zFCYHPLK3y/hSH5SEJwwPY11l6JGdC1Ue8Zzaj7f//axUs/hTC0UZaEE+a ++5v4gbqOcigKaFs9Lc3Bj8b/lE10Y ++=i2TA ++-----END PGP PUBLIC KEY BLOCK-----""", ++ }, ++ ) ++ self.assertFalse(_called_with_root(mock)) ++ ++ def test_info_gpg_key_extended(self): ++ """ ++ Test info_gpg_keys from an extended output ++ """ ++ info = """Name : gpg-pubkey ++Version : 3dbdc284 ++Release : 53674dd4 ++Architecture: (none) ++Install Date: Fri 08 Mar 2019 11:57:44 AM UTC ++Group : Public Keys ++Size : 0 ++License : pubkey ++Signature : (none) ++Source RPM : (none) ++Build Date : Mon 05 May 2014 10:37:40 AM UTC ++Build Host : localhost ++Packager : openSUSE Project Signing Key ++Summary : gpg(openSUSE Project Signing Key ) ++Description : ++-----BEGIN PGP PUBLIC KEY BLOCK----- ++Version: rpm-4.14.2.1 (NSS-3) ++ ++mQENBEkUTD8BCADWLy5d5IpJedHQQSXkC1VK/oAZlJEeBVpSZjMCn8LiHaI9Wq3G ++3Vp6wvsP1b3kssJGzVFNctdXt5tjvOLxvrEfRJuGfqHTKILByqLzkeyWawbFNfSQ ++93/8OunfSTXC1Sx3hgsNXQuOrNVKrDAQUqT620/jj94xNIg09bLSxsjN6EeTvyiO ++mtE9H1J03o9tY6meNL/gcQhxBvwuo205np0JojYBP0pOfN8l9hnIOLkA0yu4ZXig ++oKOVmf4iTjX4NImIWldT+UaWTO18NWcCrujtgHueytwYLBNV5N0oJIP2VYuLZfSD ++VYuPllv7c6O2UEOXJsdbQaVuzU1HLocDyipnABEBAAG0NG9wZW5TVVNFIFByb2pl ++Y3QgU2lnbmluZyBLZXkgPG9wZW5zdXNlQG9wZW5zdXNlLm9yZz6JATwEEwECACYC ++GwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCU2dN1AUJHR8ElQAKCRC4iy/UPb3C ++hGQrB/9teCZ3Nt8vHE0SC5NmYMAE1Spcjkzx6M4r4C70AVTMEQh/8BvgmwkKP/qI ++CWo2vC1hMXRgLg/TnTtFDq7kW+mHsCXmf5OLh2qOWCKi55Vitlf6bmH7n+h34Sha ++Ei8gAObSpZSF8BzPGl6v0QmEaGKM3O1oUbbB3Z8i6w21CTg7dbU5vGR8Yhi9rNtr ++hqrPS+q2yftjNbsODagaOUb85ESfQGx/LqoMePD+7MqGpAXjKMZqsEDP0TbxTwSk ++4UKnF4zFCYHPLK3y/hSH5SEJwwPY11l6JGdC1Ue8Zzaj7f//axUs/hTC0UZaEE+a ++5v4gbqOcigKaFs9Lc3Bj8b/lE10Y ++=i2TA ++-----END PGP PUBLIC KEY BLOCK----- ++ ++Distribution: (none) ++""" ++ mock = MagicMock(return_value=info) ++ with patch.dict(rpm.__salt__, {"cmd.run_stdout": mock}): ++ self.assertEqual( ++ rpm.info_gpg_key("key"), ++ { ++ "Name": "gpg-pubkey", ++ "Version": "3dbdc284", ++ "Release": "53674dd4", ++ "Architecture": None, ++ "Install Date": datetime.datetime(2019, 3, 8, 11, 57, 44), ++ "Group": "Public Keys", ++ "Size": 0, ++ "License": "pubkey", ++ "Signature": None, ++ "Source RPM": None, ++ "Build Date": datetime.datetime(2014, 5, 5, 10, 37, 40), ++ "Build Host": "localhost", ++ "Packager": "openSUSE Project Signing Key ", ++ "Summary": "gpg(openSUSE Project Signing Key )", ++ "Description": """-----BEGIN PGP PUBLIC KEY BLOCK----- ++Version: rpm-4.14.2.1 (NSS-3) ++ ++mQENBEkUTD8BCADWLy5d5IpJedHQQSXkC1VK/oAZlJEeBVpSZjMCn8LiHaI9Wq3G ++3Vp6wvsP1b3kssJGzVFNctdXt5tjvOLxvrEfRJuGfqHTKILByqLzkeyWawbFNfSQ ++93/8OunfSTXC1Sx3hgsNXQuOrNVKrDAQUqT620/jj94xNIg09bLSxsjN6EeTvyiO ++mtE9H1J03o9tY6meNL/gcQhxBvwuo205np0JojYBP0pOfN8l9hnIOLkA0yu4ZXig ++oKOVmf4iTjX4NImIWldT+UaWTO18NWcCrujtgHueytwYLBNV5N0oJIP2VYuLZfSD ++VYuPllv7c6O2UEOXJsdbQaVuzU1HLocDyipnABEBAAG0NG9wZW5TVVNFIFByb2pl ++Y3QgU2lnbmluZyBLZXkgPG9wZW5zdXNlQG9wZW5zdXNlLm9yZz6JATwEEwECACYC ++GwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAUCU2dN1AUJHR8ElQAKCRC4iy/UPb3C ++hGQrB/9teCZ3Nt8vHE0SC5NmYMAE1Spcjkzx6M4r4C70AVTMEQh/8BvgmwkKP/qI ++CWo2vC1hMXRgLg/TnTtFDq7kW+mHsCXmf5OLh2qOWCKi55Vitlf6bmH7n+h34Sha ++Ei8gAObSpZSF8BzPGl6v0QmEaGKM3O1oUbbB3Z8i6w21CTg7dbU5vGR8Yhi9rNtr ++hqrPS+q2yftjNbsODagaOUb85ESfQGx/LqoMePD+7MqGpAXjKMZqsEDP0TbxTwSk ++4UKnF4zFCYHPLK3y/hSH5SEJwwPY11l6JGdC1Ue8Zzaj7f//axUs/hTC0UZaEE+a ++5v4gbqOcigKaFs9Lc3Bj8b/lE10Y ++=i2TA ++-----END PGP PUBLIC KEY BLOCK-----""", ++ "Distribution": None, ++ }, ++ ) ++ self.assertFalse(_called_with_root(mock)) ++ ++ def test_remove_gpg_key(self): ++ """ ++ Test remove_gpg_key ++ """ ++ mock = MagicMock(return_value=0) ++ with patch.dict(rpm.__salt__, {"cmd.retcode": mock}): ++ self.assertTrue(rpm.remove_gpg_key("gpg-pubkey-1")) ++ self.assertFalse(_called_with_root(mock)) +diff --git a/tests/unit/modules/test_yumpkg.py b/tests/unit/modules/test_yumpkg.py +index 9fbe3d051e..dfe00a7181 100644 +--- a/tests/unit/modules/test_yumpkg.py ++++ b/tests/unit/modules/test_yumpkg.py +@@ -10,15 +10,17 @@ from tests.support.unit import TestCase, skipIf + from tests.support.mock import ( + Mock, + MagicMock, ++ mock_open, + patch, + ) + + # Import Salt libs +-from salt.exceptions import CommandExecutionError ++from salt.exceptions import CommandExecutionError, SaltInvocationError + import salt.modules.rpm_lowpkg as rpm + from salt.ext import six + import salt.modules.yumpkg as yumpkg + import salt.modules.pkg_resource as pkg_resource ++import salt.utils.platform + + try: + import pytest +@@ -799,8 +801,45 @@ class YumTestCase(TestCase, LoaderModuleMockMixin): + with pytest.raises(CommandExecutionError): + yumpkg._get_yum_config() + ++ def test_get_repo_keys(self): ++ salt_mock = {"lowpkg.list_gpg_keys": MagicMock(return_value=True)} ++ with patch.dict(yumpkg.__salt__, salt_mock): ++ self.assertTrue(yumpkg.get_repo_keys(info=True, root="/mnt")) ++ salt_mock["lowpkg.list_gpg_keys"].assert_called_once_with(True, "/mnt") + +-@skipIf(pytest is None, 'PyTest is missing') ++ def test_add_repo_key_fail(self): ++ with self.assertRaises(SaltInvocationError): ++ yumpkg.add_repo_key() ++ ++ with self.assertRaises(SaltInvocationError): ++ yumpkg.add_repo_key(path="path", text="text") ++ ++ def test_add_repo_key_path(self): ++ salt_mock = { ++ "cp.cache_file": MagicMock(return_value="path"), ++ "lowpkg.import_gpg_key": MagicMock(return_value=True), ++ } ++ with patch("salt.utils.files.fopen", mock_open(read_data="text")), patch.dict( ++ yumpkg.__salt__, salt_mock ++ ): ++ self.assertTrue(yumpkg.add_repo_key(path="path", root="/mnt")) ++ salt_mock["cp.cache_file"].assert_called_once_with("path", "base") ++ salt_mock["lowpkg.import_gpg_key"].assert_called_once_with("text", "/mnt") ++ ++ def test_add_repo_key_text(self): ++ salt_mock = {"lowpkg.import_gpg_key": MagicMock(return_value=True)} ++ with patch.dict(yumpkg.__salt__, salt_mock): ++ self.assertTrue(yumpkg.add_repo_key(text="text", root="/mnt")) ++ salt_mock["lowpkg.import_gpg_key"].assert_called_once_with("text", "/mnt") ++ ++ def test_del_repo_key(self): ++ salt_mock = {"lowpkg.remove_gpg_key": MagicMock(return_value=True)} ++ with patch.dict(yumpkg.__salt__, salt_mock): ++ self.assertTrue(yumpkg.del_repo_key(keyid="keyid", root="/mnt")) ++ salt_mock["lowpkg.remove_gpg_key"].assert_called_once_with("keyid", "/mnt") ++ ++ ++@skipIf(pytest is None, "PyTest is missing") + class YumUtilsTestCase(TestCase, LoaderModuleMockMixin): + ''' + Yum/Dnf utils tests. +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index 8cc84485b5..1f2a7dc4b2 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -22,7 +22,7 @@ from tests.support.mock import ( + import salt.utils.files + import salt.modules.zypperpkg as zypper + import salt.modules.pkg_resource as pkg_resource +-from salt.exceptions import CommandExecutionError ++from salt.exceptions import CommandExecutionError, SaltInvocationError + + # Import 3rd-party libs + from salt.ext.six.moves import configparser +@@ -1728,3 +1728,41 @@ pattern() = package-c""" + python_shell=False, + env={"ZYPP_READONLY_HACK": "1"}, + ) ++ self.assertEqual(zypper.__context__, {"pkg.other_data": None}) ++ ++ def test_get_repo_keys(self): ++ salt_mock = {"lowpkg.list_gpg_keys": MagicMock(return_value=True)} ++ with patch.dict(zypper.__salt__, salt_mock): ++ self.assertTrue(zypper.get_repo_keys(info=True, root="/mnt")) ++ salt_mock["lowpkg.list_gpg_keys"].assert_called_once_with(True, "/mnt") ++ ++ def test_add_repo_key_fail(self): ++ with self.assertRaises(SaltInvocationError): ++ zypper.add_repo_key() ++ ++ with self.assertRaises(SaltInvocationError): ++ zypper.add_repo_key(path="path", text="text") ++ ++ def test_add_repo_key_path(self): ++ salt_mock = { ++ "cp.cache_file": MagicMock(return_value="path"), ++ "lowpkg.import_gpg_key": MagicMock(return_value=True), ++ } ++ with patch("salt.utils.files.fopen", mock_open(read_data="text")), patch.dict( ++ zypper.__salt__, salt_mock ++ ): ++ self.assertTrue(zypper.add_repo_key(path="path", root="/mnt")) ++ salt_mock["cp.cache_file"].assert_called_once_with("path", "base") ++ salt_mock["lowpkg.import_gpg_key"].assert_called_once_with("text", "/mnt") ++ ++ def test_add_repo_key_text(self): ++ salt_mock = {"lowpkg.import_gpg_key": MagicMock(return_value=True)} ++ with patch.dict(zypper.__salt__, salt_mock): ++ self.assertTrue(zypper.add_repo_key(text="text", root="/mnt")) ++ salt_mock["lowpkg.import_gpg_key"].assert_called_once_with("text", "/mnt") ++ ++ def test_del_repo_key(self): ++ salt_mock = {"lowpkg.remove_gpg_key": MagicMock(return_value=True)} ++ with patch.dict(zypper.__salt__, salt_mock): ++ self.assertTrue(zypper.del_repo_key(keyid="keyid", root="/mnt")) ++ salt_mock["lowpkg.remove_gpg_key"].assert_called_once_with("keyid", "/mnt") +diff --git a/tests/unit/states/test_pkgrepo.py b/tests/unit/states/test_pkgrepo.py +new file mode 100644 +index 0000000000..9d8d88abd9 +--- /dev/null ++++ b/tests/unit/states/test_pkgrepo.py +@@ -0,0 +1,527 @@ ++""" ++ :codeauthor: Tyler Johnson ++""" ++import salt.states.pkgrepo as pkgrepo ++import salt.utils.platform ++from tests.support.mixins import LoaderModuleMockMixin ++from tests.support.mock import MagicMock, patch ++from tests.support.unit import TestCase, skipIf ++ ++ ++class PkgrepoTestCase(TestCase, LoaderModuleMockMixin): ++ """ ++ Test cases for salt.states.pkgrepo ++ """ ++ ++ def setup_loader_modules(self): ++ return { ++ pkgrepo: { ++ "__opts__": {"test": True}, ++ "__grains__": {"os": "", "os_family": ""}, ++ } ++ } ++ ++ def test_new_key_url(self): ++ """ ++ Test when only the key_url is changed that a change is triggered ++ """ ++ kwargs = { ++ "name": "deb http://mock/ sid main", ++ "disabled": False, ++ } ++ key_url = "http://mock/changed_gpg.key" ++ ++ with patch.dict( ++ pkgrepo.__salt__, {"pkg.get_repo": MagicMock(return_value=kwargs)} ++ ): ++ ret = pkgrepo.managed(key_url=key_url, **kwargs) ++ self.assertDictEqual( ++ {"key_url": {"old": None, "new": key_url}}, ret["changes"] ++ ) ++ ++ def test_update_key_url(self): ++ """ ++ Test when only the key_url is changed that a change is triggered ++ """ ++ kwargs = { ++ "name": "deb http://mock/ sid main", ++ "gpgcheck": 1, ++ "disabled": False, ++ "key_url": "http://mock/gpg.key", ++ } ++ changed_kwargs = kwargs.copy() ++ changed_kwargs["key_url"] = "http://mock/gpg2.key" ++ ++ with patch.dict( ++ pkgrepo.__salt__, {"pkg.get_repo": MagicMock(return_value=kwargs)} ++ ): ++ ret = pkgrepo.managed(**changed_kwargs) ++ self.assertIn("key_url", ret["changes"], "Expected a change to key_url") ++ self.assertDictEqual( ++ { ++ "key_url": { ++ "old": kwargs["key_url"], ++ "new": changed_kwargs["key_url"], ++ } ++ }, ++ ret["changes"], ++ ) ++ ++ def test__normalize_repo_suse(self): ++ repo = { ++ "name": "repo name", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ } ++ grains = {"os_family": "Suse"} ++ with patch.dict(pkgrepo.__grains__, grains): ++ self.assertEqual( ++ pkgrepo._normalize_repo(repo), ++ {"humanname": "repo name", "refresh": True, "priority": 0}, ++ ) ++ ++ def test__normalize_key_rpm(self): ++ key = {"Description": "key", "Date": "Date", "Other": "Other"} ++ for os_family in ("Suse", "RedHat"): ++ grains = {"os_family": os_family} ++ with patch.dict(pkgrepo.__grains__, grains): ++ self.assertEqual(pkgrepo._normalize_key(key), {"key": "key"}) ++ ++ def test__repos_keys_migrate_drop_migrate_to_empty(self): ++ src_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-2": { ++ "name": "repo name 2", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ tgt_repos = {} ++ ++ src_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key2": {"Description": "key2", "Other": "Other2"}, ++ } ++ tgt_keys = {} ++ ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "pkg.list_repos": MagicMock(side_effect=[src_repos, tgt_repos]), ++ "lowpkg.list_gpg_keys": MagicMock(side_effect=[src_keys, tgt_keys]), ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo._repos_keys_migrate_drop("/mnt", False, False), ++ ( ++ { ++ ( ++ "repo-1", ++ ( ++ ("humanname", "repo name 1"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ ( ++ "repo-2", ++ ( ++ ("humanname", "repo name 2"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ set(), ++ set(), ++ set(), ++ ), ++ ) ++ ++ def test__repos_keys_migrate_drop_migrate_to_empty_keys(self): ++ src_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-2": { ++ "name": "repo name 2", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ tgt_repos = {} ++ ++ src_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key2": {"Description": "key2", "Other": "Other2"}, ++ } ++ tgt_keys = {} ++ ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "pkg.list_repos": MagicMock(side_effect=[src_repos, tgt_repos]), ++ "lowpkg.list_gpg_keys": MagicMock(side_effect=[src_keys, tgt_keys]), ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo._repos_keys_migrate_drop("/mnt", True, False), ++ ( ++ { ++ ( ++ "repo-1", ++ ( ++ ("humanname", "repo name 1"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ ( ++ "repo-2", ++ ( ++ ("humanname", "repo name 2"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ set(), ++ {("key1", (("key", "key1"),)), ("key2", (("key", "key2"),))}, ++ set(), ++ ), ++ ) ++ ++ def test__repos_keys_migrate_drop_migrate_to_populated_no_drop(self): ++ src_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-2": { ++ "name": "repo name 2", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ tgt_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-3": { ++ "name": "repo name 3", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ ++ src_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key2": {"Description": "key2", "Other": "Other2"}, ++ } ++ tgt_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key3": {"Description": "key3", "Other": "Other2"}, ++ } ++ ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "pkg.list_repos": MagicMock(side_effect=[src_repos, tgt_repos]), ++ "lowpkg.list_gpg_keys": MagicMock(side_effect=[src_keys, tgt_keys]), ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo._repos_keys_migrate_drop("/mnt", True, False), ++ ( ++ { ++ ( ++ "repo-2", ++ ( ++ ("humanname", "repo name 2"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ set(), ++ {("key2", (("key", "key2"),))}, ++ set(), ++ ), ++ ) ++ ++ def test__repos_keys_migrate_drop_migrate_to_populated_drop(self): ++ src_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-2": { ++ "name": "repo name 2", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ tgt_repos = { ++ "repo-1": { ++ "name": "repo name 1", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": True, ++ }, ++ "repo-3": { ++ "name": "repo name 3", ++ "autorefresh": True, ++ "priority": 0, ++ "pkg_gpgcheck": False, ++ }, ++ } ++ ++ src_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key2": {"Description": "key2", "Other": "Other2"}, ++ } ++ tgt_keys = { ++ "key1": {"Description": "key1", "Other": "Other1"}, ++ "key3": {"Description": "key3", "Other": "Other2"}, ++ } ++ ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "pkg.list_repos": MagicMock(side_effect=[src_repos, tgt_repos]), ++ "lowpkg.list_gpg_keys": MagicMock(side_effect=[src_keys, tgt_keys]), ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo._repos_keys_migrate_drop("/mnt", True, True), ++ ( ++ { ++ ( ++ "repo-2", ++ ( ++ ("humanname", "repo name 2"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ { ++ ( ++ "repo-3", ++ ( ++ ("humanname", "repo name 3"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ {("key2", (("key", "key2"),))}, ++ {("key3", (("key", "key3"),))}, ++ ), ++ ) ++ ++ @skipIf(salt.utils.platform.is_windows(), "Do not run on Windows") ++ def test__copy_repository_to_suse(self): ++ grains = {"os_family": "Suse"} ++ salt_mock = {"file.copy": MagicMock()} ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ pkgrepo._copy_repository_to("/mnt") ++ salt_mock["file.copy"].assert_called_with( ++ src="/etc/zypp/repos.d", dst="/mnt/etc/zypp/repos.d", recurse=True ++ ) ++ ++ def test_migrated_non_supported_platform(self): ++ grains = {"os_family": "Debian"} ++ with patch.dict(pkgrepo.__grains__, grains): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt"), ++ { ++ "name": "/mnt", ++ "result": False, ++ "changes": {}, ++ "comment": "Migration not supported for this platform", ++ }, ++ ) ++ ++ def test_migrated_missing_keys_api(self): ++ grains = {"os_family": "Suse"} ++ with patch.dict(pkgrepo.__grains__, grains): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt"), ++ { ++ "name": "/mnt", ++ "result": False, ++ "changes": {}, ++ "comment": "Keys cannot be migrated for this platform", ++ }, ++ ) ++ ++ def test_migrated_wrong_method(self): ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "lowpkg.import_gpg_key": True, ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt", method="magic"), ++ { ++ "name": "/mnt", ++ "result": False, ++ "changes": {}, ++ "comment": "Migration method not supported", ++ }, ++ ) ++ ++ @patch("salt.states.pkgrepo._repos_keys_migrate_drop") ++ def test_migrated_empty(self, _repos_keys_migrate_drop): ++ _repos_keys_migrate_drop.return_value = (set(), set(), set(), set()) ++ ++ grains = {"os_family": "Suse"} ++ salt_mock = { ++ "lowpkg.import_gpg_key": True, ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__salt__, salt_mock ++ ): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt"), ++ { ++ "name": "/mnt", ++ "result": True, ++ "changes": {}, ++ "comment": "Repositories are already migrated", ++ }, ++ ) ++ ++ @patch("salt.states.pkgrepo._repos_keys_migrate_drop") ++ def test_migrated(self, _repos_keys_migrate_drop): ++ _repos_keys_migrate_drop.side_effect = [ ++ ( ++ { ++ ( ++ "repo-1", ++ ( ++ ("humanname", "repo name 1"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ { ++ ( ++ "repo-2", ++ ( ++ ("humanname", "repo name 2"), ++ ("priority", 0), ++ ("refresh", True), ++ ), ++ ), ++ }, ++ {("key1", (("key", "key1"),))}, ++ {("key2", (("key", "key2"),))}, ++ ), ++ (set(), set(), set(), set()), ++ ] ++ ++ grains = {"os_family": "Suse"} ++ opts = {"test": False} ++ salt_mock = { ++ "pkg.mod_repo": MagicMock(), ++ "pkg.del_repo": MagicMock(), ++ "lowpkg.import_gpg_key": MagicMock(), ++ "lowpkg.remove_gpg_key": MagicMock(), ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__opts__, opts ++ ), patch.dict(pkgrepo.__salt__, salt_mock): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt", True, True), ++ { ++ "name": "/mnt", ++ "result": True, ++ "changes": { ++ "repos migrated": ["repo-1"], ++ "repos dropped": ["repo-2"], ++ "keys migrated": ["key1"], ++ "keys dropped": ["key2"], ++ }, ++ "comment": "Repositories synchronized", ++ }, ++ ) ++ salt_mock["pkg.mod_repo"].assert_called_with( ++ "repo-1", humanname="repo name 1", priority=0, refresh=True, root="/mnt" ++ ) ++ salt_mock["pkg.del_repo"].assert_called_with("repo-2", root="/mnt") ++ salt_mock["lowpkg.import_gpg_key"].assert_called_with("key1", root="/mnt") ++ salt_mock["lowpkg.remove_gpg_key"].assert_called_with("key2", root="/mnt") ++ ++ @patch("salt.states.pkgrepo._repos_keys_migrate_drop") ++ def test_migrated_test(self, _repos_keys_migrate_drop): ++ _repos_keys_migrate_drop.return_value = ( ++ { ++ ( ++ "repo-1", ++ (("humanname", "repo name 1"), ("priority", 0), ("refresh", True)), ++ ), ++ }, ++ { ++ ( ++ "repo-2", ++ (("humanname", "repo name 2"), ("priority", 0), ("refresh", True)), ++ ), ++ }, ++ {("key1", (("key", "key1"),))}, ++ {("key2", (("key", "key2"),))}, ++ ) ++ ++ grains = {"os_family": "Suse"} ++ opts = {"test": True} ++ salt_mock = { ++ "lowpkg.import_gpg_key": True, ++ } ++ with patch.dict(pkgrepo.__grains__, grains), patch.dict( ++ pkgrepo.__opts__, opts ++ ), patch.dict(pkgrepo.__salt__, salt_mock): ++ self.assertEqual( ++ pkgrepo.migrated("/mnt", True, True), ++ { ++ "name": "/mnt", ++ "result": None, ++ "changes": { ++ "repos to migrate": ["repo-1"], ++ "repos to drop": ["repo-2"], ++ "keys to migrate": ["key1"], ++ "keys to drop": ["key2"], ++ }, ++ "comment": "There are keys or repositories to migrate or drop", ++ }, ++ ) +-- +2.29.1 + + diff --git a/fix-for-bsc-1102248-psutil-is-broken-and-so-process-.patch b/fix-for-bsc-1102248-psutil-is-broken-and-so-process-.patch new file mode 100644 index 0000000..9bde3a4 --- /dev/null +++ b/fix-for-bsc-1102248-psutil-is-broken-and-so-process-.patch @@ -0,0 +1,738 @@ +From c3d8ef9d1387ac3d69fbbd1f8042bf89ba87821a Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Tue, 13 Oct 2020 09:28:39 +0300 +Subject: [PATCH] Fix for bsc#1102248 - psutil is broken and so Process + is not working on Python 3 as it is implemented + +--- + salt/modules/ps.py | 268 ++++++++++++++++++++++++++------------------- + 1 file changed, 157 insertions(+), 111 deletions(-) + +diff --git a/salt/modules/ps.py b/salt/modules/ps.py +index bb37873f48..9925e29968 100644 +--- a/salt/modules/ps.py ++++ b/salt/modules/ps.py +@@ -1,31 +1,33 @@ + # -*- coding: utf-8 -*- +-''' ++""" + A salt interface to psutil, a system and process library. + See http://code.google.com/p/psutil. + + :depends: - psutil Python module, version 0.3.0 or later + - python-utmp package (optional) +-''' ++""" + + # Import python libs +-from __future__ import absolute_import, unicode_literals, print_function +-import time ++from __future__ import absolute_import, print_function, unicode_literals ++ + import datetime + import re ++import time + + # Import salt libs + import salt.utils.data +-from salt.exceptions import SaltInvocationError, CommandExecutionError + + # Import third party libs + import salt.utils.decorators.path ++from salt.exceptions import CommandExecutionError, SaltInvocationError + from salt.ext import six ++ + # pylint: disable=import-error + try: + import salt.utils.psutil_compat as psutil + + HAS_PSUTIL = True +- PSUTIL2 = getattr(psutil, 'version_info', ()) >= (2, 0) ++ PSUTIL2 = getattr(psutil, "version_info", ()) >= (2, 0) + except ImportError: + HAS_PSUTIL = False + # pylint: enable=import-error +@@ -33,7 +35,10 @@ except ImportError: + + def __virtual__(): + if not HAS_PSUTIL: +- return False, 'The ps module cannot be loaded: python module psutil not installed.' ++ return ( ++ False, ++ "The ps module cannot be loaded: python module psutil not installed.", ++ ) + + # Functions and attributes used in this execution module seem to have been + # added as of psutil 0.3.0, from an inspection of the source code. Only +@@ -44,15 +49,20 @@ def __virtual__(): + # as of Dec. 2013 EPEL is on 0.6.1, Debian 7 is on 0.5.1, etc.). + if psutil.version_info >= (0, 3, 0): + return True +- return (False, 'The ps execution module cannot be loaded: the psutil python module version {0} is less than 0.3.0'.format(psutil.version_info)) ++ return ( ++ False, ++ "The ps execution module cannot be loaded: the psutil python module version {0} is less than 0.3.0".format( ++ psutil.version_info ++ ), ++ ) + + + def _get_proc_cmdline(proc): +- ''' ++ """ + Returns the cmdline of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + try: + return salt.utils.data.decode(proc.cmdline() if PSUTIL2 else proc.cmdline) + except (psutil.NoSuchProcess, psutil.AccessDenied): +@@ -60,23 +70,25 @@ def _get_proc_cmdline(proc): + + + def _get_proc_create_time(proc): +- ''' ++ """ + Returns the create_time of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + try: +- return salt.utils.data.decode(proc.create_time() if PSUTIL2 else proc.create_time) ++ return salt.utils.data.decode( ++ proc.create_time() if PSUTIL2 else proc.create_time ++ ) + except (psutil.NoSuchProcess, psutil.AccessDenied): + return None + + + def _get_proc_name(proc): +- ''' ++ """ + Returns the name of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + try: + return salt.utils.data.decode(proc.name() if PSUTIL2 else proc.name) + except (psutil.NoSuchProcess, psutil.AccessDenied): +@@ -84,11 +96,11 @@ def _get_proc_name(proc): + + + def _get_proc_status(proc): +- ''' ++ """ + Returns the status of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + try: + return salt.utils.data.decode(proc.status() if PSUTIL2 else proc.status) + except (psutil.NoSuchProcess, psutil.AccessDenied): +@@ -96,11 +108,11 @@ def _get_proc_status(proc): + + + def _get_proc_username(proc): +- ''' ++ """ + Returns the username of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + try: + return salt.utils.data.decode(proc.username() if PSUTIL2 else proc.username) + except (psutil.NoSuchProcess, psutil.AccessDenied, KeyError): +@@ -108,16 +120,16 @@ def _get_proc_username(proc): + + + def _get_proc_pid(proc): +- ''' ++ """ + Returns the pid of a Process instance. + + It's backward compatible with < 2.0 versions of psutil. +- ''' ++ """ + return proc.pid + + + def top(num_processes=5, interval=3): +- ''' ++ """ + Return a list of top CPU consuming processes during the interval. + num_processes = return the top N CPU consuming processes + interval = the number of seconds to sample CPU usage over +@@ -129,57 +141,63 @@ def top(num_processes=5, interval=3): + salt '*' ps.top + + salt '*' ps.top 5 10 +- ''' ++ """ + result = [] + start_usage = {} + for pid in psutil.pids(): + try: + process = psutil.Process(pid) +- user, system = process.cpu_times() +- except ValueError: +- user, system, _, _ = process.cpu_times() + except psutil.NoSuchProcess: + continue ++ else: ++ try: ++ user, system = process.cpu_times()[:2] ++ except psutil.ZombieProcess: ++ user = system = 0.0 + start_usage[process] = user + system + time.sleep(interval) + usage = set() + for process, start in six.iteritems(start_usage): + try: +- user, system = process.cpu_times() +- except ValueError: +- user, system, _, _ = process.cpu_times() ++ user, system = process.cpu_times()[:2] + except psutil.NoSuchProcess: + continue + now = user + system + diff = now - start + usage.add((diff, process)) + +- for idx, (diff, process) in enumerate(reversed(sorted(usage))): +- if num_processes and idx >= num_processes: +- break +- if len(_get_proc_cmdline(process)) == 0: +- cmdline = _get_proc_name(process) +- else: +- cmdline = _get_proc_cmdline(process) +- info = {'cmd': cmdline, +- 'user': _get_proc_username(process), +- 'status': _get_proc_status(process), +- 'pid': _get_proc_pid(process), +- 'create_time': _get_proc_create_time(process), +- 'cpu': {}, +- 'mem': {}, ++ for diff, process in sorted(usage, key=lambda x: x[0], reverse=True): ++ info = { ++ "cmd": _get_proc_cmdline(process) or _get_proc_name(process), ++ "user": _get_proc_username(process), ++ "status": _get_proc_status(process), ++ "pid": _get_proc_pid(process), ++ "create_time": _get_proc_create_time(process), ++ "cpu": {}, ++ "mem": {}, + } +- for key, value in six.iteritems(process.cpu_times()._asdict()): +- info['cpu'][key] = value +- for key, value in six.iteritems(process.memory_info()._asdict()): +- info['mem'][key] = value ++ try: ++ for key, value in six.iteritems(process.cpu_times()._asdict()): ++ info["cpu"][key] = value ++ for key, value in six.iteritems(process.memory_info()._asdict()): ++ info["mem"][key] = value ++ except psutil.NoSuchProcess: ++ # Process ended since psutil.pids() was run earlier in this ++ # function. Ignore this process and do not include this process in ++ # the return data. ++ continue ++ + result.append(info) + ++ # Stop gathering process info since we've reached the desired number ++ if len(result) >= num_processes: ++ break ++ + return result + + + def get_pid_list(): +- ''' ++ """ + Return a list of process ids (PIDs) for all running processes. + + CLI Example: +@@ -187,12 +205,12 @@ def get_pid_list(): + .. code-block:: bash + + salt '*' ps.get_pid_list +- ''' ++ """ + return psutil.pids() + + + def proc_info(pid, attrs=None): +- ''' ++ """ + Return a dictionary of information for a process id (PID). + + CLI Example: +@@ -209,7 +227,7 @@ def proc_info(pid, attrs=None): + Optional list of desired process attributes. The list of possible + attributes can be found here: + http://pythonhosted.org/psutil/#psutil.Process +- ''' ++ """ + try: + proc = psutil.Process(pid) + return proc.as_dict(attrs) +@@ -218,7 +236,7 @@ def proc_info(pid, attrs=None): + + + def kill_pid(pid, signal=15): +- ''' ++ """ + Kill a process by PID. + + .. code-block:: bash +@@ -239,7 +257,7 @@ def kill_pid(pid, signal=15): + .. code-block:: bash + + salt 'minion' ps.kill_pid 2000 signal=9 +- ''' ++ """ + try: + psutil.Process(pid).send_signal(signal) + return True +@@ -248,7 +266,7 @@ def kill_pid(pid, signal=15): + + + def pkill(pattern, user=None, signal=15, full=False): +- ''' ++ """ + Kill processes matching a pattern. + + .. code-block:: bash +@@ -283,12 +301,15 @@ def pkill(pattern, user=None, signal=15, full=False): + .. code-block:: bash + + salt '*' ps.pkill bash signal=9 user=tom +- ''' ++ """ + + killed = [] + for proc in psutil.process_iter(): +- name_match = pattern in ' '.join(_get_proc_cmdline(proc)) if full \ ++ name_match = ( ++ pattern in " ".join(_get_proc_cmdline(proc)) ++ if full + else pattern in _get_proc_name(proc) ++ ) + user_match = True if user is None else user == _get_proc_username(proc) + if name_match and user_match: + try: +@@ -299,11 +320,11 @@ def pkill(pattern, user=None, signal=15, full=False): + if not killed: + return None + else: +- return {'killed': killed} ++ return {"killed": killed} + + +-def pgrep(pattern, user=None, full=False): +- ''' ++def pgrep(pattern, user=None, full=False, pattern_is_regex=False): ++ """ + Return the pids for processes matching a pattern. + + If full is true, the full command line is searched for a match, +@@ -323,6 +344,12 @@ def pgrep(pattern, user=None, full=False): + A boolean value indicating whether only the name of the command or + the full command line should be matched against the pattern. + ++ pattern_is_regex ++ This flag enables ps.pgrep to mirror the regex search functionality ++ found in the pgrep command line utility. ++ ++ .. versionadded:: 3001 ++ + **Examples:** + + Find all httpd processes on all 'www' minions: +@@ -336,20 +363,34 @@ def pgrep(pattern, user=None, full=False): + .. code-block:: bash + + salt '*' ps.pgrep bash user=tom +- ''' ++ """ + + procs = [] ++ ++ if pattern_is_regex: ++ pattern = re.compile(str(pattern)) ++ + for proc in psutil.process_iter(): +- name_match = pattern in ' '.join(_get_proc_cmdline(proc)) if full \ +- else pattern in _get_proc_name(proc) ++ if full: ++ process_line = " ".join(_get_proc_cmdline(proc)) ++ else: ++ process_line = _get_proc_name(proc) ++ ++ if pattern_is_regex: ++ name_match = re.search(pattern, process_line) ++ else: ++ name_match = pattern in process_line ++ + user_match = True if user is None else user == _get_proc_username(proc) ++ + if name_match and user_match: + procs.append(_get_proc_pid(proc)) ++ + return procs or None + + + def cpu_percent(interval=0.1, per_cpu=False): +- ''' ++ """ + Return the percent of time the CPU is busy. + + interval +@@ -363,7 +404,7 @@ def cpu_percent(interval=0.1, per_cpu=False): + .. code-block:: bash + + salt '*' ps.cpu_percent +- ''' ++ """ + if per_cpu: + result = list(psutil.cpu_percent(interval, True)) + else: +@@ -372,7 +413,7 @@ def cpu_percent(interval=0.1, per_cpu=False): + + + def cpu_times(per_cpu=False): +- ''' ++ """ + Return the percent of time the CPU spends in each state, + e.g. user, system, idle, nice, iowait, irq, softirq. + +@@ -385,7 +426,7 @@ def cpu_times(per_cpu=False): + .. code-block:: bash + + salt '*' ps.cpu_times +- ''' ++ """ + if per_cpu: + result = [dict(times._asdict()) for times in psutil.cpu_times(True)] + else: +@@ -394,7 +435,7 @@ def cpu_times(per_cpu=False): + + + def virtual_memory(): +- ''' ++ """ + .. versionadded:: 2014.7.0 + + Return a dict that describes statistics about system memory usage. +@@ -408,15 +449,15 @@ def virtual_memory(): + .. code-block:: bash + + salt '*' ps.virtual_memory +- ''' ++ """ + if psutil.version_info < (0, 6, 0): +- msg = 'virtual_memory is only available in psutil 0.6.0 or greater' ++ msg = "virtual_memory is only available in psutil 0.6.0 or greater" + raise CommandExecutionError(msg) + return dict(psutil.virtual_memory()._asdict()) + + + def swap_memory(): +- ''' ++ """ + .. versionadded:: 2014.7.0 + + Return a dict that describes swap memory statistics. +@@ -430,15 +471,15 @@ def swap_memory(): + .. code-block:: bash + + salt '*' ps.swap_memory +- ''' ++ """ + if psutil.version_info < (0, 6, 0): +- msg = 'swap_memory is only available in psutil 0.6.0 or greater' ++ msg = "swap_memory is only available in psutil 0.6.0 or greater" + raise CommandExecutionError(msg) + return dict(psutil.swap_memory()._asdict()) + + + def disk_partitions(all=False): +- ''' ++ """ + Return a list of disk partitions and their device, mount point, and + filesystem type. + +@@ -451,14 +492,13 @@ def disk_partitions(all=False): + .. code-block:: bash + + salt '*' ps.disk_partitions +- ''' +- result = [dict(partition._asdict()) for partition in +- psutil.disk_partitions(all)] ++ """ ++ result = [dict(partition._asdict()) for partition in psutil.disk_partitions(all)] + return result + + + def disk_usage(path): +- ''' ++ """ + Given a path, return a dict listing the total available space as well as + the free space, and used space. + +@@ -467,12 +507,12 @@ def disk_usage(path): + .. code-block:: bash + + salt '*' ps.disk_usage /home +- ''' ++ """ + return dict(psutil.disk_usage(path)._asdict()) + + + def disk_partition_usage(all=False): +- ''' ++ """ + Return a list of disk partitions plus the mount point, filesystem and usage + statistics. + +@@ -481,15 +521,15 @@ def disk_partition_usage(all=False): + .. code-block:: bash + + salt '*' ps.disk_partition_usage +- ''' ++ """ + result = disk_partitions(all) + for partition in result: +- partition.update(disk_usage(partition['mountpoint'])) ++ partition.update(disk_usage(partition["mountpoint"])) + return result + + + def total_physical_memory(): +- ''' ++ """ + Return the total number of bytes of physical memory. + + CLI Example: +@@ -497,9 +537,9 @@ def total_physical_memory(): + .. code-block:: bash + + salt '*' ps.total_physical_memory +- ''' ++ """ + if psutil.version_info < (0, 6, 0): +- msg = 'virtual_memory is only available in psutil 0.6.0 or greater' ++ msg = "virtual_memory is only available in psutil 0.6.0 or greater" + raise CommandExecutionError(msg) + try: + return psutil.virtual_memory().total +@@ -510,7 +550,7 @@ def total_physical_memory(): + + + def num_cpus(): +- ''' ++ """ + Return the number of CPUs. + + CLI Example: +@@ -518,7 +558,7 @@ def num_cpus(): + .. code-block:: bash + + salt '*' ps.num_cpus +- ''' ++ """ + try: + return psutil.cpu_count() + except AttributeError: +@@ -528,7 +568,7 @@ def num_cpus(): + + + def boot_time(time_format=None): +- ''' ++ """ + Return the boot time in number of seconds since the epoch began. + + CLI Example: +@@ -545,7 +585,7 @@ def boot_time(time_format=None): + .. code-block:: bash + + salt '*' ps.boot_time +- ''' ++ """ + try: + b_time = int(psutil.boot_time()) + except AttributeError: +@@ -558,12 +598,12 @@ def boot_time(time_format=None): + try: + return b_time.strftime(time_format) + except TypeError as exc: +- raise SaltInvocationError('Invalid format string: {0}'.format(exc)) ++ raise SaltInvocationError("Invalid format string: {0}".format(exc)) + return b_time + + + def network_io_counters(interface=None): +- ''' ++ """ + Return network I/O statistics. + + CLI Example: +@@ -573,7 +613,7 @@ def network_io_counters(interface=None): + salt '*' ps.network_io_counters + + salt '*' ps.network_io_counters interface=eth0 +- ''' ++ """ + if not interface: + return dict(psutil.net_io_counters()._asdict()) + else: +@@ -585,7 +625,7 @@ def network_io_counters(interface=None): + + + def disk_io_counters(device=None): +- ''' ++ """ + Return disk I/O statistics. + + CLI Example: +@@ -595,7 +635,7 @@ def disk_io_counters(device=None): + salt '*' ps.disk_io_counters + + salt '*' ps.disk_io_counters device=sda1 +- ''' ++ """ + if not device: + return dict(psutil.disk_io_counters()._asdict()) + else: +@@ -607,7 +647,7 @@ def disk_io_counters(device=None): + + + def get_users(): +- ''' ++ """ + Return logged-in users. + + CLI Example: +@@ -615,7 +655,7 @@ def get_users(): + .. code-block:: bash + + salt '*' ps.get_users +- ''' ++ """ + try: + recs = psutil.users() + return [dict(x._asdict()) for x in recs] +@@ -634,14 +674,20 @@ def get_users(): + started = rec[8] + if isinstance(started, tuple): + started = started[0] +- result.append({'name': rec[4], 'terminal': rec[2], +- 'started': started, 'host': rec[5]}) ++ result.append( ++ { ++ "name": rec[4], ++ "terminal": rec[2], ++ "started": started, ++ "host": rec[5], ++ } ++ ) + except ImportError: + return False + + + def lsof(name): +- ''' ++ """ + Retrieve the lsof information of the given process name. + + CLI Example: +@@ -649,17 +695,17 @@ def lsof(name): + .. code-block:: bash + + salt '*' ps.lsof apache2 +- ''' ++ """ + sanitize_name = six.text_type(name) +- lsof_infos = __salt__['cmd.run']("lsof -c " + sanitize_name) ++ lsof_infos = __salt__["cmd.run"]("lsof -c " + sanitize_name) + ret = [] + ret.extend([sanitize_name, lsof_infos]) + return ret + + +-@salt.utils.decorators.path.which('netstat') ++@salt.utils.decorators.path.which("netstat") + def netstat(name): +- ''' ++ """ + Retrieve the netstat information of the given process name. + + CLI Example: +@@ -667,9 +713,9 @@ def netstat(name): + .. code-block:: bash + + salt '*' ps.netstat apache2 +- ''' ++ """ + sanitize_name = six.text_type(name) +- netstat_infos = __salt__['cmd.run']("netstat -nap") ++ netstat_infos = __salt__["cmd.run"]("netstat -nap") + found_infos = [] + ret = [] + for info in netstat_infos.splitlines(): +@@ -679,9 +725,9 @@ def netstat(name): + return ret + + +-@salt.utils.decorators.path.which('ss') ++@salt.utils.decorators.path.which("ss") + def ss(name): +- ''' ++ """ + Retrieve the ss information of the given process name. + + CLI Example: +@@ -692,9 +738,9 @@ def ss(name): + + .. versionadded:: 2016.11.6 + +- ''' ++ """ + sanitize_name = six.text_type(name) +- ss_infos = __salt__['cmd.run']("ss -neap") ++ ss_infos = __salt__["cmd.run"]("ss -neap") + found_infos = [] + ret = [] + for info in ss_infos.splitlines(): +@@ -705,7 +751,7 @@ def ss(name): + + + def psaux(name): +- ''' ++ """ + Retrieve information corresponding to a "ps aux" filtered + with the given pattern. It could be just a name or a regular + expression (using python search from "re" module). +@@ -715,11 +761,11 @@ def psaux(name): + .. code-block:: bash + + salt '*' ps.psaux www-data.+apache2 +- ''' ++ """ + sanitize_name = six.text_type(name) + pattern = re.compile(sanitize_name) + salt_exception_pattern = re.compile("salt.+ps.psaux.+") +- ps_aux = __salt__['cmd.run']("ps aux") ++ ps_aux = __salt__["cmd.run"]("ps aux") + found_infos = [] + ret = [] + nb_lines = 0 +-- +2.29.1 + + diff --git a/grains-master-can-read-grains.patch b/grains-master-can-read-grains.patch new file mode 100644 index 0000000..e6cae6b --- /dev/null +++ b/grains-master-can-read-grains.patch @@ -0,0 +1,34 @@ +From 0b6106815b708bc4cf25b4a02ebc8b7ebf299b39 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Tue, 27 Oct 2020 13:16:37 +0100 +Subject: [PATCH] grains: master can read grains + +--- + salt/grains/extra.py | 10 ++++++++-- + 1 file changed, 8 insertions(+), 2 deletions(-) + +diff --git a/salt/grains/extra.py b/salt/grains/extra.py +index 6a26aece77..f80061ff4e 100644 +--- a/salt/grains/extra.py ++++ b/salt/grains/extra.py +@@ -94,8 +94,14 @@ def __secure_boot(): + 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" ++ # The minion is usually running as a privileged user, but is ++ # not the case for the master. Seems that the master can also ++ # pick the grains, and this file can only be readed by "root" ++ try: ++ with salt.utils.files.fopen(sboot[0], "rb") as fd: ++ enabled = fd.read()[-1:] == b"\x01" ++ except PermissionError: ++ pass + return enabled + + +-- +2.29.1 + + diff --git a/pkgrepo-support-python-2.7-function-call-295.patch b/pkgrepo-support-python-2.7-function-call-295.patch new file mode 100644 index 0000000..e63b2a1 --- /dev/null +++ b/pkgrepo-support-python-2.7-function-call-295.patch @@ -0,0 +1,26 @@ +From a487f19e6a0ed6b4b7e987e5b6d90852050eb2d9 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Mon, 16 Nov 2020 10:37:29 +0100 +Subject: [PATCH] pkgrepo: support Python 2.7 function call (#295) + +--- + salt/states/pkgrepo.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/salt/states/pkgrepo.py b/salt/states/pkgrepo.py +index 6c42d17d32..504f1425c7 100644 +--- a/salt/states/pkgrepo.py ++++ b/salt/states/pkgrepo.py +@@ -818,7 +818,7 @@ def migrated(name, keys=True, drop=False, method=None, **kwargs): + if method == "copy": + _copy_repository_to(name) + else: +- __salt__["pkg.mod_repo"](repo, **dict(repo_info), root=name) ++ __salt__["pkg.mod_repo"](repo, root=name, **dict(repo_info)) + for repo, _ in repos_to_drop: + __salt__["pkg.del_repo"](repo, root=name) + +-- +2.28.0 + + diff --git a/salt.changes b/salt.changes index 1d28930..54442a5 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,37 @@ +------------------------------------------------------------------- +Mon Nov 16 09:48:45 UTC 2020 - Pablo Suárez Hernández + +- Fix syntax error on pkgrepo state with Python 2.7 +- transactional_update: unify with chroot.call + +- Added: + * pkgrepo-support-python-2.7-function-call-295.patch + * transactional_update-unify-with-chroot.call.patch + +------------------------------------------------------------------- +Tue Nov 10 15:43:09 UTC 2020 - Jochen Breuer + +- Add "migrated" state and GPG key management functions + +- Added: + * add-migrated-state-and-gpg-key-management-functions-.patch + +------------------------------------------------------------------- +Tue Nov 10 15:09:16 UTC 2020 - Jochen Breuer + +- Master can read grains + +- Added: + * grains-master-can-read-grains.patch + +------------------------------------------------------------------- +Tue Nov 10 14:03:22 UTC 2020 - Jochen Breuer + +- Fix for broken psutil (bsc#1102248) + +- Added: + * fix-for-bsc-1102248-psutil-is-broken-and-so-process-.patch + ------------------------------------------------------------------- Fri Nov 6 09:19:22 UTC 2020 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index 8a28880..5ec08a9 100644 --- a/salt.spec +++ b/salt.spec @@ -371,6 +371,17 @@ Patch142: fix-novendorchange-option-284.patch Patch143: fix-cve-2020-25592-and-add-tests-bsc-1178319.patch # PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/293 Patch144: set-passphrase-for-salt-ssh-keys-to-empty-string-293.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/279 +Patch145: fix-for-bsc-1102248-psutil-is-broken-and-so-process-.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/58520 +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/286 +Patch146: grains-master-can-read-grains.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/58784 +Patch147: add-migrated-state-and-gpg-key-management-functions-.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/292 +Patch148: transactional_update-unify-with-chroot.call.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/295 +Patch149: pkgrepo-support-python-2.7-function-call-295.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -1022,6 +1033,11 @@ cp %{S:5} ./.travis.yml %patch142 -p1 %patch143 -p1 %patch144 -p1 +%patch145 -p1 +%patch146 -p1 +%patch147 -p1 +%patch148 -p1 +%patch149 -p1 %build # Putting /usr/bin at the front of $PATH is needed for RHEL/RES 7. Without this diff --git a/transactional_update-unify-with-chroot.call.patch b/transactional_update-unify-with-chroot.call.patch new file mode 100644 index 0000000..a822683 --- /dev/null +++ b/transactional_update-unify-with-chroot.call.patch @@ -0,0 +1,110 @@ +From 383cb53b9936b8ff1d8707c74daf5001add2dd20 Mon Sep 17 00:00:00 2001 +From: Alberto Planas +Date: Wed, 4 Nov 2020 16:34:47 +0100 +Subject: [PATCH] transactional_update: unify with chroot.call + +Return for both .call() "retcode" when fail +--- + salt/modules/chroot.py | 5 +++-- + salt/modules/transactional_update.py | 4 ++-- + tests/unit/modules/test_chroot.py | 6 ++++-- + tests/unit/modules/test_transactional_update.py | 12 ++++++++++-- + 4 files changed, 19 insertions(+), 8 deletions(-) + +diff --git a/salt/modules/chroot.py b/salt/modules/chroot.py +index 5e890b5c35..fbec3ea788 100644 +--- a/salt/modules/chroot.py ++++ b/salt/modules/chroot.py +@@ -192,10 +192,11 @@ def call(root, function, *args, **kwargs): + if isinstance(local, dict) and 'retcode' in local: + __context__['retcode'] = local['retcode'] + return local.get('return', data) +- except (KeyError, ValueError): ++ except ValueError: + return { + 'result': False, +- 'comment': "Can't parse container command output" ++ 'retcode': ret['retcode'], ++ 'comment': {'stdout': ret['stdout'], 'stderr': ret['stderr']}, + } + finally: + __utils__['files.rm_rf'](thin_dest_path) +diff --git a/salt/modules/transactional_update.py b/salt/modules/transactional_update.py +index 9b14557e07..7bbdb697b8 100644 +--- a/salt/modules/transactional_update.py ++++ b/salt/modules/transactional_update.py +@@ -988,8 +988,8 @@ def call(function, *args, **kwargs): + if isinstance(local, dict) and "retcode" in local: + __context__["retcode"] = local["retcode"] + return local.get("return", data) +- except (KeyError, ValueError): +- return {"result": False, "comment": ret_stdout} ++ except ValueError: ++ return {"result": False, "retcode": 1, "comment": ret_stdout} + finally: + __utils__["files.rm_rf"](thin_dest_path) + +diff --git a/tests/unit/modules/test_chroot.py b/tests/unit/modules/test_chroot.py +index 045d56c5b0..eb7b8cb4aa 100644 +--- a/tests/unit/modules/test_chroot.py ++++ b/tests/unit/modules/test_chroot.py +@@ -145,13 +145,14 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin): + utils_mock = { + 'thin.gen_thin': MagicMock(return_value='/salt-thin.tgz'), + 'files.rm_rf': MagicMock(), +- 'json.find_json': MagicMock(return_value={'return': {}}) ++ 'json.find_json': MagicMock(side_effect=ValueError()) + } + salt_mock = { + 'cmd.run': MagicMock(return_value=''), + 'config.option': MagicMock(), + 'cmd.run_chroot': MagicMock(return_value={ + 'retcode': 1, ++ 'stdout': '', + 'stderr': 'Error', + }), + } +@@ -159,7 +160,8 @@ class ChrootTestCase(TestCase, LoaderModuleMockMixin): + patch.dict(chroot.__salt__, salt_mock): + self.assertEqual(chroot.call('/chroot', 'test.ping'), { + 'result': False, +- 'comment': "Can't parse container command output" ++ 'retcode': 1, ++ 'comment': {'stdout': '', 'stderr': 'Error'}, + }) + utils_mock['thin.gen_thin'].assert_called_once() + salt_mock['config.option'].assert_called() +diff --git a/tests/unit/modules/test_transactional_update.py b/tests/unit/modules/test_transactional_update.py +index b42734a53d..4616e0968f 100644 +--- a/tests/unit/modules/test_transactional_update.py ++++ b/tests/unit/modules/test_transactional_update.py +@@ -372,7 +372,11 @@ class TransactionalUpdateTestCase(TestCase, LoaderModuleMockMixin): + 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"} ++ assert tu.call("test.ping") == { ++ "result": False, ++ "retcode": 1, ++ "comment": "Error", ++ } + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() +@@ -424,7 +428,11 @@ class TransactionalUpdateTestCase(TestCase, LoaderModuleMockMixin): + 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"} ++ assert tu.call("test.ping") == { ++ "result": False, ++ "retcode": 1, ++ "comment": "Not found", ++ } + + utils_mock["thin.gen_thin"].assert_called_once() + salt_mock["config.option"].assert_called() +-- +2.29.1 + +