diff --git a/_lastrevision b/_lastrevision index bb91596..dbca5c3 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -f20138622e17e52fd49e531edd607b46d08a146c \ No newline at end of file +e07459bfeea39239f6b446f40f6502e72dea488f \ No newline at end of file diff --git a/add-support-for-gpgautoimport-539.patch b/add-support-for-gpgautoimport-539.patch new file mode 100644 index 0000000..3ead610 --- /dev/null +++ b/add-support-for-gpgautoimport-539.patch @@ -0,0 +1,369 @@ +From fbd5163bd0d5409a1823e9fb8e0cb623c22d6036 Mon Sep 17 00:00:00 2001 +From: Michael Calmer +Date: Fri, 8 Jul 2022 10:15:37 +0200 +Subject: [PATCH] add support for gpgautoimport (#539) + +* add support for gpgautoimport to refresh_db in the zypperpkg module + +* call refresh_db function from mod_repo + +* call refresh_db with kwargs where possible + +* ignore no repos defined exit code + +* fix zypperpkg test after adding more success return codes +--- + salt/modules/zypperpkg.py | 47 +++++++--- + tests/unit/modules/test_zypperpkg.py | 124 +++++++++++++++++++++++---- + 2 files changed, 140 insertions(+), 31 deletions(-) + +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index 39d26f0e93..b622105e15 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -591,7 +591,7 @@ def list_upgrades(refresh=True, root=None, **kwargs): + salt '*' pkg.list_upgrades + """ + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + ret = dict() + cmd = ["list-updates"] +@@ -705,7 +705,7 @@ def info_available(*names, **kwargs): + + # Refresh db before extracting the latest package + if kwargs.get("refresh", True): +- refresh_db(root) ++ refresh_db(root, **kwargs) + + pkg_info = [] + batch = names[:] +@@ -1395,7 +1395,6 @@ def mod_repo(repo, **kwargs): + cmd_opt.append("--name='{}'".format(kwargs.get("humanname"))) + + if kwargs.get("gpgautoimport") is True: +- global_cmd_opt.append("--gpg-auto-import-keys") + call_refresh = True + + if cmd_opt: +@@ -1407,8 +1406,8 @@ def mod_repo(repo, **kwargs): + # when used with "zypper ar --refresh" or "zypper mr --refresh" + # --gpg-auto-import-keys is not doing anything + # so we need to specifically refresh here with --gpg-auto-import-keys +- refresh_opts = global_cmd_opt + ["refresh"] + [repo] +- __zypper__(root=root).xml.call(*refresh_opts) ++ kwargs.update({"repos": repo}) ++ refresh_db(root=root, **kwargs) + elif not added and not cmd_opt: + comment = "Specified arguments did not result in modification of repo" + +@@ -1419,7 +1418,7 @@ def mod_repo(repo, **kwargs): + return repo + + +-def refresh_db(force=None, root=None): ++def refresh_db(force=None, root=None, **kwargs): + """ + Trigger a repository refresh by calling ``zypper refresh``. Refresh will run + with ``--force`` if the "force=True" flag is passed on the CLI or +@@ -1430,6 +1429,17 @@ def refresh_db(force=None, root=None): + + {'': Bool} + ++ gpgautoimport : False ++ If set to True, automatically trust and import public GPG key for ++ the repository. ++ ++ .. versionadded:: 3005 ++ ++ repos ++ Refresh just the specified repos ++ ++ .. versionadded:: 3005 ++ + root + operate on a different root directory. + +@@ -1450,11 +1460,22 @@ def refresh_db(force=None, root=None): + salt.utils.pkg.clear_rtag(__opts__) + ret = {} + refresh_opts = ["refresh"] ++ global_opts = [] + if force is None: + force = __pillar__.get("zypper", {}).get("refreshdb_force", True) + if force: + refresh_opts.append("--force") +- out = __zypper__(root=root).refreshable.call(*refresh_opts) ++ repos = kwargs.get("repos", []) ++ refresh_opts.extend([repos] if not isinstance(repos, list) else repos) ++ ++ if kwargs.get("gpgautoimport", False): ++ global_opts.append("--gpg-auto-import-keys") ++ ++ # We do the actual call to zypper refresh. ++ # We ignore retcode 6 which is returned when there are no repositories defined. ++ out = __zypper__(root=root).refreshable.call( ++ *global_opts, *refresh_opts, success_retcodes=[0, 6] ++ ) + + for line in out.splitlines(): + if not line: +@@ -1639,7 +1660,7 @@ def install( + 'arch': ''}}} + """ + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + try: + pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"]( +@@ -1934,7 +1955,7 @@ def upgrade( + cmd_update.insert(0, "--no-gpg-checks") + + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + if dryrun: + cmd_update.append("--dry-run") +@@ -2844,7 +2865,7 @@ def search(criteria, refresh=False, **kwargs): + root = kwargs.get("root", None) + + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + cmd = ["search"] + if kwargs.get("match") == "exact": +@@ -2995,7 +3016,7 @@ def download(*packages, **kwargs): + + refresh = kwargs.get("refresh", False) + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + pkg_ret = {} + for dld_result in ( +@@ -3147,7 +3168,7 @@ def list_patches(refresh=False, root=None, **kwargs): + salt '*' pkg.list_patches + """ + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + return _get_patches(root=root) + +@@ -3241,7 +3262,7 @@ def resolve_capabilities(pkgs, refresh=False, root=None, **kwargs): + salt '*' pkg.resolve_capabilities resolve_capabilities=True w3m_ssl + """ + if refresh: +- refresh_db(root) ++ refresh_db(root, **kwargs) + + ret = list() + for pkg in pkgs: +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index fea6eeb004..3f1560a385 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -358,7 +358,12 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + run_out = {"stderr": "", "stdout": "\n".join(ref_out), "retcode": 0} + + zypper_mock = MagicMock(return_value=run_out) +- call_kwargs = {"output_loglevel": "trace", "python_shell": False, "env": {}} ++ call_kwargs = { ++ "output_loglevel": "trace", ++ "python_shell": False, ++ "env": {}, ++ "success_retcodes": [0, 6], ++ } + with patch.dict(zypper.__salt__, {"cmd.run_all": zypper_mock}): + with patch.object(salt.utils.pkg, "clear_rtag", Mock()): + result = zypper.refresh_db() +@@ -376,6 +381,73 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + zypper_mock.assert_called_with( + ["zypper", "--non-interactive", "refresh", "--force"], **call_kwargs + ) ++ zypper.refresh_db(gpgautoimport=True) ++ zypper_mock.assert_called_with( ++ [ ++ "zypper", ++ "--non-interactive", ++ "--gpg-auto-import-keys", ++ "refresh", ++ "--force", ++ ], ++ **call_kwargs ++ ) ++ zypper.refresh_db(gpgautoimport=True, force=True) ++ zypper_mock.assert_called_with( ++ [ ++ "zypper", ++ "--non-interactive", ++ "--gpg-auto-import-keys", ++ "refresh", ++ "--force", ++ ], ++ **call_kwargs ++ ) ++ zypper.refresh_db(gpgautoimport=True, force=False) ++ zypper_mock.assert_called_with( ++ [ ++ "zypper", ++ "--non-interactive", ++ "--gpg-auto-import-keys", ++ "refresh", ++ ], ++ **call_kwargs ++ ) ++ zypper.refresh_db( ++ gpgautoimport=True, ++ refresh=True, ++ repos="mock-repo-name", ++ root=None, ++ url="http://repo.url/some/path", ++ ) ++ zypper_mock.assert_called_with( ++ [ ++ "zypper", ++ "--non-interactive", ++ "--gpg-auto-import-keys", ++ "refresh", ++ "--force", ++ "mock-repo-name", ++ ], ++ **call_kwargs ++ ) ++ zypper.refresh_db( ++ gpgautoimport=True, ++ repos="mock-repo-name", ++ root=None, ++ url="http://repo.url/some/path", ++ ) ++ zypper_mock.assert_called_with( ++ [ ++ "zypper", ++ "--non-interactive", ++ "--gpg-auto-import-keys", ++ "refresh", ++ "--force", ++ "mock-repo-name", ++ ], ++ **call_kwargs ++ ) + + def test_info_installed(self): + """ +@@ -1555,18 +1627,23 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + + url = self.new_repo_config["url"] + name = self.new_repo_config["name"] +- with zypper_patcher: ++ with zypper_patcher, patch.object(zypper, "refresh_db", Mock()) as refreshmock: + zypper.mod_repo(name, **{"url": url, "gpgautoimport": True}) + self.assertEqual( + zypper.__zypper__(root=None).xml.call.call_args_list, + [ + call("ar", url, name), +- call("--gpg-auto-import-keys", "refresh", name), + ], + ) + self.assertTrue( + zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0 + ) ++ refreshmock.assert_called_once_with( ++ gpgautoimport=True, ++ repos=name, ++ root=None, ++ url="http://repo.url/some/path", ++ ) + + def test_repo_noadd_nomod_ref(self): + """ +@@ -1585,15 +1662,17 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + "salt.modules.zypperpkg", **self.zypper_patcher_config + ) + +- with zypper_patcher: ++ with zypper_patcher, patch.object(zypper, "refresh_db", Mock()) as refreshmock: + zypper.mod_repo(name, **{"url": url, "gpgautoimport": True}) +- self.assertEqual( +- zypper.__zypper__(root=None).xml.call.call_args_list, +- [call("--gpg-auto-import-keys", "refresh", name)], +- ) + self.assertTrue( + zypper.__zypper__(root=None).refreshable.xml.call.call_count == 0 + ) ++ refreshmock.assert_called_once_with( ++ gpgautoimport=True, ++ repos=name, ++ root=None, ++ url="http://repo.url/some/path", ++ ) + + def test_repo_add_mod_ref(self): + """ +@@ -1606,10 +1685,10 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + zypper_patcher = patch.multiple( + "salt.modules.zypperpkg", **self.zypper_patcher_config + ) +- + url = self.new_repo_config["url"] + name = self.new_repo_config["name"] +- with zypper_patcher: ++ ++ with zypper_patcher, patch.object(zypper, "refresh_db", Mock()) as refreshmock: + zypper.mod_repo( + name, **{"url": url, "refresh": True, "gpgautoimport": True} + ) +@@ -1617,11 +1696,17 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + zypper.__zypper__(root=None).xml.call.call_args_list, + [ + call("ar", url, name), +- call("--gpg-auto-import-keys", "refresh", name), + ], + ) + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( +- "--gpg-auto-import-keys", "mr", "--refresh", name ++ "mr", "--refresh", name ++ ) ++ refreshmock.assert_called_once_with( ++ gpgautoimport=True, ++ refresh=True, ++ repos=name, ++ root=None, ++ url="http://repo.url/some/path", + ) + + def test_repo_noadd_mod_ref(self): +@@ -1641,16 +1726,19 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + "salt.modules.zypperpkg", **self.zypper_patcher_config + ) + +- with zypper_patcher: ++ with zypper_patcher, patch.object(zypper, "refresh_db", Mock()) as refreshmock: + zypper.mod_repo( + name, **{"url": url, "refresh": True, "gpgautoimport": True} + ) +- self.assertEqual( +- zypper.__zypper__(root=None).xml.call.call_args_list, +- [call("--gpg-auto-import-keys", "refresh", name)], +- ) + zypper.__zypper__(root=None).refreshable.xml.call.assert_called_once_with( +- "--gpg-auto-import-keys", "mr", "--refresh", name ++ "mr", "--refresh", name ++ ) ++ refreshmock.assert_called_once_with( ++ gpgautoimport=True, ++ refresh=True, ++ repos=name, ++ root=None, ++ url="http://repo.url/some/path", + ) + + def test_wildcard_to_query_match_all(self): +-- +2.36.1 + + diff --git a/add-support-for-name-pkgs-and-diff_attr-parameters-t.patch b/add-support-for-name-pkgs-and-diff_attr-parameters-t.patch new file mode 100644 index 0000000..90d257d --- /dev/null +++ b/add-support-for-name-pkgs-and-diff_attr-parameters-t.patch @@ -0,0 +1,1057 @@ +From c162e36fc52ca2f10b25354f1e430e13113f2976 Mon Sep 17 00:00:00 2001 +From: Alexander Graul +Date: Thu, 7 Jul 2022 11:26:34 +0200 +Subject: [PATCH] Add support for name, pkgs and diff_attr parameters + to zypperpkg.upgrade()/yumpkg.upgrade() - backport 3004 (#538) + +* Migrate zypper.upgrade tests to pytest + +(cherry picked from commit ecce005b543f66198c7ac118966254dd3d60682f) + +* Add names and pkgs parameters to zypper.upgrade + +Fixes https://github.com/saltstack/salt/issues/62030 + +(cherry picked from commit 19ebb40dc4538c983721a8746a201b7f1300c2f7) + +* Don't turn attr="all" into a list + +pkg_resource.format_pkg_list expects its `attr` argument to be either a +list of attributes or the string "all" to indicate all available +attributes should be used for formatting. + +Fixes: https://github.com/saltstack/salt/issues/62032 +(cherry picked from commit 05482da89b91442235d3cc2889e59ac3722a7fae) + +* Add diff_attr parameter to zypper/yum upgrade + +diff_attr works just like it does for pkg.install. Having the +option to return additional attributes can remove the need for a +follow-up list_pkgs call. + +Fixes: https://github.com/saltstack/salt/issues/62031 +(cherry picked from commit 20ffffe3be6c7d94e9cc3338a57bbf5014f33d93) +--- + changelog/62030.fixed | 1 + + changelog/62031.added | 1 + + changelog/62032.fixed | 1 + + salt/modules/yumpkg.py | 7 +- + salt/modules/zypperpkg.py | 76 ++- + tests/pytests/unit/modules/test_zypperpkg.py | 278 ++++++++++- + tests/unit/modules/test_zypperpkg.py | 482 ------------------- + 7 files changed, 356 insertions(+), 490 deletions(-) + create mode 100644 changelog/62030.fixed + create mode 100644 changelog/62031.added + create mode 100644 changelog/62032.fixed + +diff --git a/changelog/62030.fixed b/changelog/62030.fixed +new file mode 100644 +index 0000000000..bd60463606 +--- /dev/null ++++ b/changelog/62030.fixed +@@ -0,0 +1 @@ ++Fix inconsitency regarding name and pkgs parameters between zypperpkg.upgrade() and yumpkg.upgrade() +diff --git a/changelog/62031.added b/changelog/62031.added +new file mode 100644 +index 0000000000..f0b66ff96f +--- /dev/null ++++ b/changelog/62031.added +@@ -0,0 +1 @@ ++Add `diff_attr` parameter to pkg.upgrade() (zypper/yum). +diff --git a/changelog/62032.fixed b/changelog/62032.fixed +new file mode 100644 +index 0000000000..ceb3cc89b9 +--- /dev/null ++++ b/changelog/62032.fixed +@@ -0,0 +1 @@ ++Fix attr=all handling in pkg.list_pkgs() (yum/zypper). +diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py +index 3138ac2e59..0013282507 100644 +--- a/salt/modules/yumpkg.py ++++ b/salt/modules/yumpkg.py +@@ -735,7 +735,7 @@ def list_pkgs(versions_as_list=False, **kwargs): + return {} + + attr = kwargs.get("attr") +- if attr is not None: ++ if attr is not None and attr != "all": + attr = salt.utils.args.split_input(attr) + + contextkey = "pkg.list_pkgs" +@@ -1835,6 +1835,7 @@ def upgrade( + normalize=True, + minimal=False, + obsoletes=True, ++ diff_attr=None, + **kwargs + ): + """ +@@ -1991,7 +1992,7 @@ def upgrade( + if salt.utils.data.is_true(refresh): + refresh_db(**kwargs) + +- old = list_pkgs() ++ old = list_pkgs(attr=diff_attr) + + targets = [] + if name or pkgs: +@@ -2023,7 +2024,7 @@ def upgrade( + cmd.extend(targets) + result = _call_yum(cmd) + __context__.pop("pkg.list_pkgs", None) +- new = list_pkgs() ++ new = list_pkgs(attr=diff_attr) + ret = salt.utils.data.compare_dicts(old, new) + + if result["retcode"] != 0: +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index ac6c36a09f..39d26f0e93 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -941,7 +941,7 @@ def list_pkgs(versions_as_list=False, root=None, includes=None, **kwargs): + return {} + + attr = kwargs.get("attr") +- if attr is not None: ++ if attr is not None and attr != "all": + attr = salt.utils.args.split_input(attr) + + includes = includes if includes else [] +@@ -1793,6 +1793,8 @@ def install( + + + def upgrade( ++ name=None, ++ pkgs=None, + refresh=True, + dryrun=False, + dist_upgrade=False, +@@ -1802,6 +1804,7 @@ def upgrade( + skip_verify=False, + no_recommends=False, + root=None, ++ diff_attr=None, + **kwargs + ): # pylint: disable=unused-argument + """ +@@ -1821,6 +1824,27 @@ def upgrade( + + Run a full system upgrade, a zypper upgrade + ++ name ++ The name of the package to be installed. Note that this parameter is ++ ignored if ``pkgs`` is passed or if ``dryrun`` is set to True. ++ ++ CLI Example: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.install name= ++ ++ pkgs ++ A list of packages to install from a software repository. Must be ++ passed as a python list. Note that this parameter is ignored if ++ ``dryrun`` is set to True. ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.install pkgs='["foo", "bar"]' ++ + refresh + force a refresh if set to True (default). + If set to False it depends on zypper if a refresh is +@@ -1852,6 +1876,24 @@ def upgrade( + root + Operate on a different root directory. + ++ diff_attr: ++ If a list of package attributes is specified, returned value will ++ contain them, eg.:: ++ ++ {'': { ++ 'old': { ++ 'version': '', ++ 'arch': ''}, ++ ++ 'new': { ++ 'version': '', ++ 'arch': ''}}} ++ ++ Valid attributes are: ``epoch``, ``version``, ``release``, ``arch``, ++ ``install_date``, ``install_date_time_t``. ++ ++ If ``all`` is specified, all valid attributes will be returned. ++ + Returns a dictionary containing the changes: + + .. code-block:: python +@@ -1859,11 +1901,27 @@ def upgrade( + {'': {'old': '', + 'new': ''}} + ++ If an attribute list is specified in ``diff_attr``, the dict will also contain ++ any specified attribute, eg.:: ++ ++ .. code-block:: python ++ ++ {'': { ++ 'old': { ++ 'version': '', ++ 'arch': ''}, ++ ++ 'new': { ++ 'version': '', ++ 'arch': ''}}} ++ + CLI Example: + + .. code-block:: bash + + salt '*' pkg.upgrade ++ salt '*' pkg.upgrade name=mypackage ++ salt '*' pkg.upgrade pkgs='["package1", "package2"]' + salt '*' pkg.upgrade dist_upgrade=True fromrepo='["MyRepoName"]' novendorchange=True + salt '*' pkg.upgrade dist_upgrade=True dryrun=True + """ +@@ -1899,12 +1957,24 @@ def upgrade( + allowvendorchange, novendorchange + ).noraise.call(*cmd_update + ["--debug-solver"]) + +- old = list_pkgs(root=root) ++ if not dist_upgrade: ++ if name or pkgs: ++ try: ++ (pkg_params, _) = __salt__["pkg_resource.parse_targets"]( ++ name=name, pkgs=pkgs, sources=None, **kwargs ++ ) ++ if pkg_params: ++ cmd_update.extend(pkg_params.keys()) ++ ++ except MinionError as exc: ++ raise CommandExecutionError(exc) ++ ++ old = list_pkgs(root=root, attr=diff_attr) + __zypper__(systemd_scope=_systemd_scope(), root=root).allow_vendor_change( + allowvendorchange, novendorchange + ).noraise.call(*cmd_update) + _clean_cache() +- new = list_pkgs(root=root) ++ new = list_pkgs(root=root, attr=diff_attr) + ret = salt.utils.data.compare_dicts(old, new) + + if __zypper__.exit_code not in __zypper__.SUCCESS_EXIT_CODES: +diff --git a/tests/pytests/unit/modules/test_zypperpkg.py b/tests/pytests/unit/modules/test_zypperpkg.py +index bfc1558c9a..e02bba9a07 100644 +--- a/tests/pytests/unit/modules/test_zypperpkg.py ++++ b/tests/pytests/unit/modules/test_zypperpkg.py +@@ -4,17 +4,31 @@ + + + import os ++import textwrap + + import pytest + import salt.modules.pkg_resource as pkg_resource + import salt.modules.zypperpkg as zypper +-from salt.exceptions import SaltInvocationError ++from salt.exceptions import CommandExecutionError, SaltInvocationError + from tests.support.mock import MagicMock, mock_open, patch + + + @pytest.fixture + def configure_loader_modules(): +- return {zypper: {"rpm": None}, pkg_resource: {}} ++ return { ++ zypper: { ++ "rpm": None, ++ "_systemd_scope": MagicMock(return_value=False), ++ "osrelease_info": [15, 3], ++ "__salt__": {"pkg_resource.parse_targets": pkg_resource.parse_targets}, ++ }, ++ pkg_resource: {"__grains__": {"os": "SUSE"}}, ++ } ++ ++ ++@pytest.fixture(autouse=True) ++def fresh_zypper_instance(): ++ zypper.__zypper__ = zypper._Zypper() + + + def test_list_pkgs_no_context(): +@@ -254,3 +268,263 @@ def test_pkg_list_holds(): + ret = zypper.list_holds() + assert len(ret) == 1 + assert "bar-2:2.3.4-2.1.*" in ret ++ ++ ++@pytest.mark.parametrize( ++ "zypper_version,lowpkg_version_cmp,expected_inst_avc,expected_dup_avc", ++ [ ++ ("0.5", [-1, -1], False, False), ++ ("1.11.34", [0, -1], False, True), ++ ("1.14.8", [0, 0], True, True), ++ ], ++) ++def test_refresh_zypper_flags( ++ zypper_version, lowpkg_version_cmp, expected_inst_avc, expected_dup_avc ++): ++ with patch( ++ "salt.modules.zypperpkg.version", MagicMock(return_value=zypper_version) ++ ), patch.dict( ++ zypper.__salt__, ++ {"lowpkg.version_cmp": MagicMock(side_effect=lowpkg_version_cmp)}, ++ ): ++ _zypper = zypper._Zypper() ++ _zypper.refresh_zypper_flags() ++ assert _zypper.inst_avc == expected_inst_avc ++ assert _zypper.dup_avc == expected_dup_avc ++ ++ ++@pytest.mark.parametrize( ++ "inst_avc,dup_avc,avc,allowvendorchange_param,novendorchange_param,expected", ++ [ ++ # inst_avc = True, dup_avc = True ++ (True, True, False, False, False, True), ++ (True, True, False, True, False, True), ++ (True, True, False, False, True, False), ++ (True, True, False, True, True, True), ++ # inst_avc = False, dup_avc = True ++ (False, True, False, False, False, True), ++ (False, True, False, True, False, True), ++ (False, True, False, False, True, False), ++ (False, True, False, True, True, True), ++ # inst_avc = False, dup_avc = False ++ (False, False, False, False, False, False), ++ (False, False, False, True, False, False), ++ (False, False, False, False, True, False), ++ (False, False, False, True, True, False), ++ ], ++) ++@patch("salt.modules.zypperpkg._Zypper.refresh_zypper_flags", MagicMock()) ++def test_allow_vendor_change( ++ inst_avc, ++ dup_avc, ++ avc, ++ allowvendorchange_param, ++ novendorchange_param, ++ expected, ++): ++ _zypper = zypper._Zypper() ++ _zypper.inst_avc = inst_avc ++ _zypper.dup_avc = dup_avc ++ _zypper.avc = avc ++ _zypper.allow_vendor_change(allowvendorchange_param, novendorchange_param) ++ assert _zypper.avc == expected ++ ++ ++@pytest.mark.parametrize( ++ "package,pre_version,post_version,fromrepo_param,name_param,pkgs_param,diff_attr_param", ++ [ ++ ("vim", "1.1", "1.2", [], "", [], "all"), ++ ("kernel-default", "1.1", "1.1,1.2", ["dummy", "dummy2"], "", [], None), ++ ("vim", "1.1", "1.2", [], "vim", [], None), ++ ], ++) ++@patch.object(zypper, "refresh_db", MagicMock(return_value=True)) ++def test_upgrade( ++ package, ++ pre_version, ++ post_version, ++ fromrepo_param, ++ name_param, ++ pkgs_param, ++ diff_attr_param, ++): ++ with patch( ++ "salt.modules.zypperpkg.__zypper__.noraise.call" ++ ) as zypper_mock, patch.object( ++ zypper, ++ "list_pkgs", ++ MagicMock(side_effect=[{package: pre_version}, {package: post_version}]), ++ ) as list_pkgs_mock: ++ expected_call = ["update", "--auto-agree-with-licenses"] ++ for repo in fromrepo_param: ++ expected_call.extend(["--repo", repo]) ++ ++ if pkgs_param: ++ expected_call.extend(pkgs_param) ++ elif name_param: ++ expected_call.append(name_param) ++ ++ result = zypper.upgrade( ++ name=name_param, ++ pkgs=pkgs_param, ++ fromrepo=fromrepo_param, ++ diff_attr=diff_attr_param, ++ ) ++ zypper_mock.assert_any_call(*expected_call) ++ assert result == {package: {"old": pre_version, "new": post_version}} ++ list_pkgs_mock.assert_any_call(root=None, attr=diff_attr_param) ++ ++ ++@pytest.mark.parametrize( ++ "package,pre_version,post_version,fromrepo_param", ++ [ ++ ("vim", "1.1", "1.2", []), ++ ("emacs", "1.1", "1.2", ["Dummy", "Dummy2"]), ++ ], ++) ++@patch.object(zypper, "refresh_db", MagicMock(return_value=True)) ++def test_dist_upgrade(package, pre_version, post_version, fromrepo_param): ++ with patch( ++ "salt.modules.zypperpkg.__zypper__.noraise.call" ++ ) as zypper_mock, patch.object( ++ zypper, ++ "list_pkgs", ++ MagicMock(side_effect=[{package: pre_version}, {package: post_version}]), ++ ): ++ expected_call = ["dist-upgrade", "--auto-agree-with-licenses"] ++ ++ for repo in fromrepo_param: ++ expected_call.extend(["--from", repo]) ++ ++ result = zypper.upgrade(dist_upgrade=True, fromrepo=fromrepo_param) ++ zypper_mock.assert_any_call(*expected_call) ++ assert result == {package: {"old": pre_version, "new": post_version}} ++ ++ ++@pytest.mark.parametrize( ++ "package,pre_version,post_version,dup_avc,novendorchange_param,allowvendorchange_param,vendor_change", ++ [ ++ # dup_avc = True, both params = default -> no vendor change ++ ("vim", "1.1", "1.2", True, True, False, False), ++ # dup_avc = True, allowvendorchange = True -> vendor change ++ ( ++ "emacs", ++ "1.1", ++ "1.2", ++ True, ++ True, ++ True, ++ True, ++ ), ++ # dup_avc = True, novendorchange = False -> vendor change ++ ("joe", "1.1", "1.2", True, False, False, True), ++ # dup_avc = True, both params = toggled -> vendor change ++ ("kate", "1.1", "1.2", True, False, True, True), ++ # dup_avc = False -> no vendor change ++ ( ++ "gedit", ++ "1.1", ++ "1.2", ++ False, ++ False, ++ True, ++ False ++ ), ++ ], ++) ++@patch.object(zypper, "refresh_db", MagicMock(return_value=True)) ++def test_dist_upgrade_vendorchange( ++ package, ++ pre_version, ++ post_version, ++ dup_avc, ++ novendorchange_param, ++ allowvendorchange_param, ++ vendor_change ++): ++ cmd_run_mock = MagicMock(return_value={"retcode": 0, "stdout": None}) ++ with patch.object( ++ zypper, ++ "list_pkgs", ++ MagicMock(side_effect=[{package: pre_version}, {package: post_version}]), ++ ), patch("salt.modules.zypperpkg.__zypper__.refresh_zypper_flags",), patch.dict( ++ zypper.__salt__, {"cmd.run_all": cmd_run_mock} ++ ): ++ expected_cmd = ["zypper", "--non-interactive", "--no-refresh", "dist-upgrade"] ++ # --allow-vendor-change is injected right after "dist-upgrade" ++ if vendor_change: ++ expected_cmd.append("--allow-vendor-change") ++ expected_cmd.append("--auto-agree-with-licenses") ++ ++ zypper.__zypper__.dup_avc = dup_avc ++ zypper.upgrade( ++ dist_upgrade=True, ++ allowvendorchange=allowvendorchange_param, ++ novendorchange=novendorchange_param, ++ ) ++ cmd_run_mock.assert_any_call( ++ expected_cmd, output_loglevel="trace", python_shell=False, env={} ++ ) ++ ++ ++@pytest.mark.parametrize( ++ "package,pre_version,post_version,fromrepo_param", ++ [ ++ ("vim", "1.1", "1.1", []), ++ ("emacs", "1.1", "1.1", ["Dummy", "Dummy2"]), ++ ], ++) ++@patch.object(zypper, "refresh_db", MagicMock(return_value=True)) ++def test_dist_upgrade_dry_run(package, pre_version, post_version, fromrepo_param): ++ with patch( ++ "salt.modules.zypperpkg.__zypper__.noraise.call" ++ ) as zypper_mock, patch.object( ++ zypper, ++ "list_pkgs", ++ MagicMock(side_effect=[{package: pre_version}, {package: post_version}]), ++ ): ++ expected_call = ["dist-upgrade", "--auto-agree-with-licenses", "--dry-run"] ++ ++ for repo in fromrepo_param: ++ expected_call.extend(["--from", repo]) ++ ++ zypper.upgrade(dist_upgrade=True, dryrun=True, fromrepo=fromrepo_param) ++ zypper_mock.assert_any_call(*expected_call) ++ # dryrun=True causes two calls, one with a trailing --debug-solver flag ++ expected_call.append("--debug-solver") ++ zypper_mock.assert_any_call(*expected_call) ++ ++ ++@patch.object(zypper, "refresh_db", MagicMock(return_value=True)) ++def test_dist_upgrade_failure(): ++ zypper_output = textwrap.dedent( ++ """\ ++ Loading repository data... ++ Reading installed packages... ++ Computing distribution upgrade... ++ Use 'zypper repos' to get the list of defined repositories. ++ Repository 'DUMMY' not found by its alias, number, or URI. ++ """ ++ ) ++ call_spy = MagicMock() ++ zypper_mock = MagicMock() ++ zypper_mock.stdout = zypper_output ++ zypper_mock.stderr = "" ++ zypper_mock.exit_code = 3 ++ zypper_mock.noraise.call = call_spy ++ with patch("salt.modules.zypperpkg.__zypper__", zypper_mock), patch.object( ++ zypper, "list_pkgs", MagicMock(side_effect=[{"vim": 1.1}, {"vim": 1.1}]) ++ ): ++ expected_call = [ ++ "dist-upgrade", ++ "--auto-agree-with-licenses", ++ "--from", ++ "Dummy", ++ ] ++ ++ with pytest.raises(CommandExecutionError) as exc: ++ zypper.upgrade(dist_upgrade=True, fromrepo=["Dummy"]) ++ call_spy.assert_called_with(*expected_call) ++ ++ assert exc.exception.info["changes"] == {} ++ assert exc.exception.info["result"]["stdout"] == zypper_output +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index 39f28f2198..fea6eeb004 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -610,151 +610,6 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + {"vim": "7.4.326-2.62", "fakepkg": ""}, + ) + +- def test_upgrade_without_vendor_change(self): +- """ +- Dist-upgrade without vendor change option. +- """ +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}]), +- ): +- ret = zypper.upgrade(dist_upgrade=True) +- self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) +- zypper_mock.assert_any_call( +- "dist-upgrade", "--auto-agree-with-licenses", +- ) +- +- def test_refresh_zypper_flags(self): +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.version", MagicMock(return_value="0.5") +- ), patch.dict( +- zypper.__salt__, {"lowpkg.version_cmp": MagicMock(side_effect=[-1, -1])} +- ): +- zypper.__zypper__.refresh_zypper_flags() +- assert zypper.__zypper__.inst_avc == False +- assert zypper.__zypper__.dup_avc == False +- with patch( +- "salt.modules.zypperpkg.version", MagicMock(return_value="1.11.34") +- ), patch.dict( +- zypper.__salt__, {"lowpkg.version_cmp": MagicMock(side_effect=[0, -1])} +- ): +- zypper.__zypper__.refresh_zypper_flags() +- assert zypper.__zypper__.inst_avc == False +- assert zypper.__zypper__.dup_avc == True +- with patch( +- "salt.modules.zypperpkg.version", MagicMock(return_value="1.14.8") +- ), patch.dict( +- zypper.__salt__, {"lowpkg.version_cmp": MagicMock(side_effect=[0, 0])} +- ): +- zypper.__zypper__.refresh_zypper_flags() +- assert zypper.__zypper__.inst_avc == True +- assert zypper.__zypper__.dup_avc == True +- +- @patch("salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock()) +- def test_allow_vendor_change_function(self): +- zypper.__zypper__._reset() +- zypper.__zypper__.inst_avc = True +- zypper.__zypper__.dup_avc = True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, False) +- assert zypper.__zypper__.avc == True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, False) +- assert zypper.__zypper__.avc == True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, True) +- assert zypper.__zypper__.avc == False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, True) +- assert zypper.__zypper__.avc == True +- +- zypper.__zypper__._reset() +- zypper.__zypper__.inst_avc = False +- zypper.__zypper__.dup_avc = True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, False) +- assert zypper.__zypper__.avc == True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, False) +- assert zypper.__zypper__.avc == True +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, True) +- assert zypper.__zypper__.avc == False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, True) +- assert zypper.__zypper__.avc == True +- +- zypper.__zypper__._reset() +- zypper.__zypper__.inst_avc = False +- zypper.__zypper__.dup_avc = False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, False) +- assert zypper.__zypper__.avc == False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, False) +- assert zypper.__zypper__.avc == False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(False, True) +- assert zypper.__zypper__.avc == False +- zypper.__zypper__.avc = False +- zypper.__zypper__.allow_vendor_change(True, True) +- assert zypper.__zypper__.avc == False +- +- @patch( +- "salt.utils.environment.get_module_environment", +- MagicMock(return_value={"SALT_RUNNING": "1"}), +- ) +- def test_zypper_call_dist_upgrade_with_avc_true(self): +- cmd_run_mock = MagicMock(return_value={"retcode": 0, "stdout": None}) +- zypper.__zypper__._reset() +- with patch.dict(zypper.__salt__, {"cmd.run_all": cmd_run_mock}), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ), patch("salt.modules.zypperpkg.__zypper__._reset", MagicMock()): +- zypper.__zypper__.dup_avc = True +- zypper.__zypper__.avc = True +- zypper.__zypper__.call("dist-upgrade") +- cmd_run_mock.assert_any_call( +- [ +- "zypper", +- "--non-interactive", +- "--no-refresh", +- "dist-upgrade", +- "--allow-vendor-change", +- ], +- output_loglevel="trace", +- python_shell=False, +- env={"SALT_RUNNING": "1"}, +- ) +- +- @patch( +- "salt.utils.environment.get_module_environment", +- MagicMock(return_value={"SALT_RUNNING": "1"}), +- ) +- def test_zypper_call_dist_upgrade_with_avc_false(self): +- cmd_run_mock = MagicMock(return_value={"retcode": 0, "stdout": None}) +- zypper.__zypper__._reset() +- with patch.dict(zypper.__salt__, {"cmd.run_all": cmd_run_mock}), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ), patch("salt.modules.zypperpkg.__zypper__._reset", MagicMock()): +- zypper.__zypper__.dup_avc = False +- zypper.__zypper__.avc = False +- zypper.__zypper__.call("dist-upgrade") +- cmd_run_mock.assert_any_call( +- ["zypper", "--non-interactive", "--no-refresh", "dist-upgrade",], +- output_loglevel="trace", +- python_shell=False, +- env={"SALT_RUNNING": "1"}, +- ) +- + @patch( + "salt.utils.environment.get_module_environment", + MagicMock(return_value={"SALT_RUNNING": "1"}), +@@ -802,296 +657,6 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + env={"SALT_RUNNING": "1"}, + ) + +- def test_upgrade_with_novendorchange_true(self): +- """ +- Dist-upgrade without vendor change option. +- """ +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ) as refresh_flags_mock, patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}]), +- ): +- ret = zypper.upgrade(dist_upgrade=True, novendorchange=True) +- refresh_flags_mock.assert_not_called() +- zypper_mock.assert_any_call( +- "dist-upgrade", "--auto-agree-with-licenses", +- ) +- +- def test_upgrade_with_novendorchange_false(self): +- """ +- Perform dist-upgrade with novendorchange set to False. +- """ +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- zypper.__zypper__.inst_avc = True +- zypper.__zypper__.dup_avc = True +- with patch.dict( +- zypper.__salt__, +- { +- "pkg_resource.version": MagicMock(return_value="1.15"), +- "lowpkg.version_cmp": MagicMock(return_value=1), +- }, +- ): +- ret = zypper.upgrade( +- dist_upgrade=True, +- dryrun=True, +- fromrepo=["Dummy", "Dummy2"], +- novendorchange=False, +- ) +- assert zypper.__zypper__.avc == True +- +- def test_upgrade_with_allowvendorchange_true(self): +- """ +- Perform dist-upgrade with allowvendorchange set to True. +- """ +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- with patch.dict( +- zypper.__salt__, +- { +- "pkg_resource.version": MagicMock(return_value="1.15"), +- "lowpkg.version_cmp": MagicMock(return_value=1), +- }, +- ): +- +- zypper.__zypper__.inst_avc = True +- zypper.__zypper__.dup_avc = True +- ret = zypper.upgrade( +- dist_upgrade=True, +- dryrun=True, +- fromrepo=["Dummy", "Dummy2"], +- allowvendorchange=True, +- ) +- assert zypper.__zypper__.avc == True +- +- def test_upgrade_with_allowvendorchange_false(self): +- """ +- Perform dist-upgrade with allowvendorchange set to False. +- """ +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- with patch.dict( +- zypper.__salt__, +- { +- "pkg_resource.version": MagicMock(return_value="1.15"), +- "lowpkg.version_cmp": MagicMock(return_value=1), +- }, +- ): +- +- zypper.__zypper__.inst_avc = True +- zypper.__zypper__.dup_avc = True +- ret = zypper.upgrade( +- dist_upgrade=True, +- dryrun=True, +- fromrepo=["Dummy", "Dummy2"], +- allowvendorchange=False, +- ) +- assert zypper.__zypper__.avc == False +- +- def test_upgrade_old_zypper(self): +- zypper.__zypper__._reset() +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg.__zypper__.refresh_zypper_flags", MagicMock() +- ) as refresh_flags_mock, patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- with patch.dict( +- zypper.__salt__, +- { +- "pkg_resource.version": MagicMock(return_value="1.11"), +- "lowpkg.version_cmp": MagicMock(return_value=-1), +- }, +- ): +- zypper.__zypper__.inst_avc = False +- zypper.__zypper__.dup_avc = False +- ret = zypper.upgrade( +- dist_upgrade=True, +- dryrun=True, +- fromrepo=["Dummy", "Dummy2"], +- novendorchange=False, +- ) +- zypper.__zypper__.avc = False +- +- def test_upgrade_success(self): +- """ +- Test system upgrade and dist-upgrade success. +- +- :return: +- """ +- with patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- with patch( +- "salt.modules.zypperpkg.__zypper__.noraise.call", MagicMock() +- ) as zypper_mock: +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.2"}]), +- ): +- ret = zypper.upgrade() +- self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.2"}}) +- zypper_mock.assert_any_call("update", "--auto-agree-with-licenses") +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock( +- side_effect=[ +- {"kernel-default": "1.1"}, +- {"kernel-default": "1.1,1.2"}, +- ] +- ), +- ): +- ret = zypper.upgrade() +- self.assertDictEqual( +- ret, {"kernel-default": {"old": "1.1", "new": "1.1,1.2"}} +- ) +- zypper_mock.assert_any_call("update", "--auto-agree-with-licenses") +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1,1.2"}]), +- ): +- ret = zypper.upgrade() +- self.assertDictEqual(ret, {"vim": {"old": "1.1", "new": "1.1,1.2"}}) +- zypper_mock.assert_any_call("update", "--auto-agree-with-licenses") +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- ret = zypper.upgrade(dist_upgrade=True, dryrun=True) +- zypper_mock.assert_any_call( +- "dist-upgrade", "--auto-agree-with-licenses", "--dry-run" +- ) +- zypper_mock.assert_any_call( +- "dist-upgrade", +- "--auto-agree-with-licenses", +- "--dry-run", +- "--debug-solver", +- ) +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- ret = zypper.upgrade( +- dist_upgrade=False, fromrepo=["Dummy", "Dummy2"], dryrun=False +- ) +- zypper_mock.assert_any_call( +- "update", +- "--auto-agree-with-licenses", +- "--repo", +- "Dummy", +- "--repo", +- "Dummy2", +- ) +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- ret = zypper.upgrade( +- dist_upgrade=True, +- dryrun=True, +- fromrepo=["Dummy", "Dummy2"], +- novendorchange=True, +- ) +- zypper_mock.assert_any_call( +- "dist-upgrade", +- "--auto-agree-with-licenses", +- "--dry-run", +- "--from", +- "Dummy", +- "--from", +- "Dummy2", +- ) +- zypper_mock.assert_any_call( +- "dist-upgrade", +- "--auto-agree-with-licenses", +- "--dry-run", +- "--from", +- "Dummy", +- "--from", +- "Dummy2", +- "--debug-solver", +- ) +- +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- ret = zypper.upgrade( +- dist_upgrade=False, fromrepo=["Dummy", "Dummy2"], dryrun=False +- ) +- zypper_mock.assert_any_call( +- "update", +- "--auto-agree-with-licenses", +- "--repo", +- "Dummy", +- "--repo", +- "Dummy2", +- ) +- + def test_upgrade_kernel(self): + """ + Test kernel package upgrade success. +@@ -1136,53 +701,6 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin): + }, + ) + +- def test_upgrade_failure(self): +- """ +- Test system upgrade failure. +- +- :return: +- """ +- zypper_out = """ +-Loading repository data... +-Reading installed packages... +-Computing distribution upgrade... +-Use 'zypper repos' to get the list of defined repositories. +-Repository 'DUMMY' not found by its alias, number, or URI. +-""" +- +- class FailingZypperDummy: +- def __init__(self): +- self.stdout = zypper_out +- self.stderr = "" +- self.pid = 1234 +- self.exit_code = 555 +- self.noraise = MagicMock() +- self.allow_vendor_change = self +- self.SUCCESS_EXIT_CODES = [0] +- +- def __call__(self, *args, **kwargs): +- return self +- +- with patch( +- "salt.modules.zypperpkg.__zypper__", FailingZypperDummy() +- ) as zypper_mock, patch( +- "salt.modules.zypperpkg.refresh_db", MagicMock(return_value=True) +- ), patch( +- "salt.modules.zypperpkg._systemd_scope", MagicMock(return_value=False) +- ): +- zypper_mock.noraise.call = MagicMock() +- with patch( +- "salt.modules.zypperpkg.list_pkgs", +- MagicMock(side_effect=[{"vim": "1.1"}, {"vim": "1.1"}]), +- ): +- with self.assertRaises(CommandExecutionError) as cmd_exc: +- ret = zypper.upgrade(dist_upgrade=True, fromrepo=["DUMMY"]) +- self.assertEqual(cmd_exc.exception.info["changes"], {}) +- self.assertEqual(cmd_exc.exception.info["result"]["stdout"], zypper_out) +- zypper_mock.noraise.call.assert_called_with( +- "dist-upgrade", "--auto-agree-with-licenses", "--from", "DUMMY", +- ) +- + def test_upgrade_available(self): + """ + Test whether or not an upgrade is available for a given package. +-- +2.36.1 + + diff --git a/fix-62092-catch-zmq.error.zmqerror-to-set-hwm-for-zm.patch b/fix-62092-catch-zmq.error.zmqerror-to-set-hwm-for-zm.patch new file mode 100644 index 0000000..6c25a31 --- /dev/null +++ b/fix-62092-catch-zmq.error.zmqerror-to-set-hwm-for-zm.patch @@ -0,0 +1,35 @@ +From df474d3cc0a5f02591fea093f9efc324c6feef46 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Thu, 7 Jul 2022 11:38:09 +0100 +Subject: [PATCH] Fix #62092: Catch zmq.error.ZMQError to set HWM for + zmq >= 3 (#543) + +It looks like before release 23.0.0, when trying to access zmq.HWM it +was raising ``AttributeError``, which is now wrapped under pyzmq's own +``zmq.error.ZMQError``. +Simply caching that, should then set the HWM correctly for zmq >= 3 +and therefore fix #62092. + +Co-authored-by: Mircea Ulinic +--- + salt/transport/zeromq.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/salt/transport/zeromq.py b/salt/transport/zeromq.py +index 9e61b23255..aa06298ee1 100644 +--- a/salt/transport/zeromq.py ++++ b/salt/transport/zeromq.py +@@ -898,7 +898,7 @@ class ZeroMQPubServerChannel(salt.transport.server.PubServerChannel): + try: + pub_sock.setsockopt(zmq.HWM, self.opts.get("pub_hwm", 1000)) + # in zmq >= 3.0, there are separate send and receive HWM settings +- except AttributeError: ++ except (AttributeError, zmq.error.ZMQError): + # Set the High Water Marks. For more information on HWM, see: + # http://api.zeromq.org/4-1:zmq-setsockopt + pub_sock.setsockopt(zmq.SNDHWM, self.opts.get("pub_hwm", 1000)) +-- +2.36.1 + + diff --git a/fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch b/fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch new file mode 100644 index 0000000..4e03076 --- /dev/null +++ b/fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch @@ -0,0 +1,83 @@ +From 65494338f5a9bdaa0be27afab3da3a03a92d8cda Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Fri, 8 Jul 2022 13:35:50 +0100 +Subject: [PATCH] fix: jinja2 contextfuntion base on version + (bsc#1198744) (#520) + +--- + salt/utils/jinja.py | 16 ++++++++++++++-- + tests/unit/utils/test_jinja.py | 8 +++++++- + 2 files changed, 21 insertions(+), 3 deletions(-) + +diff --git a/salt/utils/jinja.py b/salt/utils/jinja.py +index 0cb70bf64a..6b5b0d4e81 100644 +--- a/salt/utils/jinja.py ++++ b/salt/utils/jinja.py +@@ -25,7 +25,7 @@ import salt.utils.json + import salt.utils.stringutils + import salt.utils.url + import salt.utils.yaml +-from jinja2 import BaseLoader, Markup, TemplateNotFound, nodes ++from jinja2 import BaseLoader, TemplateNotFound, nodes + from jinja2.environment import TemplateModule + from jinja2.exceptions import TemplateRuntimeError + from jinja2.ext import Extension +@@ -34,6 +34,12 @@ from salt.utils.decorators.jinja import jinja_filter, jinja_global, jinja_test + from salt.utils.odict import OrderedDict + from salt.utils.versions import LooseVersion + ++try: ++ from markupsafe import Markup ++except ImportError: ++ # jinja < 3.1 ++ from jinja2 import Markup ++ + log = logging.getLogger(__name__) + + __all__ = ["SaltCacheLoader", "SerializerExtension"] +@@ -706,7 +712,13 @@ def method_call(obj, f_name, *f_args, **f_kwargs): + return getattr(obj, f_name, lambda *args, **kwargs: None)(*f_args, **f_kwargs) + + +-@jinja2.contextfunction ++try: ++ contextfunction = jinja2.contextfunction ++except AttributeError: ++ contextfunction = jinja2.pass_context ++ ++ ++@contextfunction + def show_full_context(ctx): + return salt.utils.data.simple_types_filter( + {key: value for key, value in ctx.items()} +diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py +index 6502831aff..6bbcf9ef6f 100644 +--- a/tests/unit/utils/test_jinja.py ++++ b/tests/unit/utils/test_jinja.py +@@ -22,7 +22,7 @@ import salt.utils.files + import salt.utils.json + import salt.utils.stringutils + import salt.utils.yaml +-from jinja2 import DictLoader, Environment, Markup, exceptions ++from jinja2 import DictLoader, Environment, exceptions + from salt.exceptions import SaltRenderError + from salt.utils.decorators.jinja import JinjaFilter + from salt.utils.jinja import ( +@@ -46,6 +46,12 @@ try: + except ImportError: + HAS_TIMELIB = False + ++try: ++ from markupsafe import Markup ++except ImportError: ++ # jinja < 3.1 ++ from jinja2 import Markup ++ + BLINESEP = salt.utils.stringutils.to_bytes(os.linesep) + + +-- +2.36.1 + + diff --git a/fix-ownership-of-salt-thin-directory-when-using-the-.patch b/fix-ownership-of-salt-thin-directory-when-using-the-.patch new file mode 100644 index 0000000..ab9a8bb --- /dev/null +++ b/fix-ownership-of-salt-thin-directory-when-using-the-.patch @@ -0,0 +1,50 @@ +From 34a81d88db3862bcc03cdda4974e576723af7643 Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Mon, 27 Jun 2022 18:03:49 +0300 +Subject: [PATCH] Fix ownership of salt thin directory when using the + Salt Bundle + +--- + salt/client/ssh/ssh_py_shim.py | 25 ++++++++++++++++++++++++- + 1 file changed, 24 insertions(+), 1 deletion(-) + +diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py +index 293ea1b7fa..95171f7aea 100644 +--- a/salt/client/ssh/ssh_py_shim.py ++++ b/salt/client/ssh/ssh_py_shim.py +@@ -292,7 +292,30 @@ def main(argv): # pylint: disable=W0613 + os.makedirs(OPTIONS.saltdir) + cache_dir = os.path.join(OPTIONS.saltdir, "running_data", "var", "cache") + os.makedirs(os.path.join(cache_dir, "salt")) +- os.symlink("salt", os.path.relpath(os.path.join(cache_dir, "venv-salt-minion"))) ++ os.symlink( ++ "salt", os.path.relpath(os.path.join(cache_dir, "venv-salt-minion")) ++ ) ++ if os.path.exists(OPTIONS.saltdir) and ( ++ "SUDO_UID" in os.environ or "SUDO_GID" in os.environ ++ ): ++ try: ++ sudo_uid = int(os.environ.get("SUDO_UID", -1)) ++ except ValueError: ++ sudo_uid = -1 ++ try: ++ sudo_gid = int(os.environ.get("SUDO_GID", -1)) ++ except ValueError: ++ sudo_gid = -1 ++ dstat = os.stat(OPTIONS.saltdir) ++ if (sudo_uid != -1 and dstat.st_uid != sudo_uid) or ( ++ sudo_gid != -1 and dstat.st_gid != sudo_gid ++ ): ++ os.chown(OPTIONS.saltdir, sudo_uid, sudo_gid) ++ for dir_path, dir_names, file_names in os.walk(OPTIONS.saltdir): ++ for dir_name in dir_names: ++ os.lchown(os.path.join(dir_path, dir_name), sudo_uid, sudo_gid) ++ for file_name in file_names: ++ os.lchown(os.path.join(dir_path, file_name), sudo_uid, sudo_gid) + + if venv_salt_call is None: + # Use Salt thin only if Salt Bundle (venv-salt-minion) is not available +-- +2.36.1 + + diff --git a/fix-salt.states.file.managed-for-follow_symlinks-tru.patch b/fix-salt.states.file.managed-for-follow_symlinks-tru.patch new file mode 100644 index 0000000..843e075 --- /dev/null +++ b/fix-salt.states.file.managed-for-follow_symlinks-tru.patch @@ -0,0 +1,308 @@ +From 10705d922a11e5f2654d26e83e9f302862fafb18 Mon Sep 17 00:00:00 2001 +From: Petr Pavlu <31453820+petrpavlu@users.noreply.github.com> +Date: Fri, 8 Jul 2022 10:11:52 +0200 +Subject: [PATCH] Fix salt.states.file.managed() for + follow_symlinks=True and test=True (bsc#1199372) (#535) + +When managing file /etc/test as follows: +> file /etc/test: +> file.managed: +> - name: /etc/test +> - source: salt://config/test +> - mode: 644 +> - follow_symlinks: True + +and with /etc/test being a symlink to a different file, an invocation of +"salt-call '*' state.apply test=True" can report that the file should be +updated even when a subsequent run of the same command without the test +parameter makes no changes. + +The problem is that the test code path doesn't take correctly into +account the follow_symlinks=True setting and ends up comparing +permissions of the symlink instead of its target file. + +The patch addresses the problem by extending functions +salt.modules.file.check_managed(), check_managed_changes() and +check_file_meta() to have the follow_symlinks parameter which gets +propagated to the salt.modules.file.stats() call and by updating +salt.states.file.managed() to forward the same parameter to +salt.modules.file.check_managed_changes(). + +Fixes #62066. + +[Cherry-picked from upstream commit +95bfbe31a2dc54723af3f1783d40de152760fe1a.] +--- + changelog/62066.fixed | 1 + + salt/modules/file.py | 27 +++- + salt/states/file.py | 1 + + .../unit/modules/file/test_file_check.py | 144 ++++++++++++++++++ + 4 files changed, 172 insertions(+), 1 deletion(-) + create mode 100644 changelog/62066.fixed + create mode 100644 tests/pytests/unit/modules/file/test_file_check.py + +diff --git a/changelog/62066.fixed b/changelog/62066.fixed +new file mode 100644 +index 0000000000..68216a03c1 +--- /dev/null ++++ b/changelog/62066.fixed +@@ -0,0 +1 @@ ++Fixed salt.states.file.managed() for follow_symlinks=True and test=True +diff --git a/salt/modules/file.py b/salt/modules/file.py +index 73619064ef..40c07455e3 100644 +--- a/salt/modules/file.py ++++ b/salt/modules/file.py +@@ -5281,11 +5281,18 @@ def check_managed( + serole=None, + setype=None, + serange=None, ++ follow_symlinks=False, + **kwargs + ): + """ + Check to see what changes need to be made for a file + ++ follow_symlinks ++ If the desired path is a symlink, follow it and check the permissions ++ of the file to which the symlink points. ++ ++ .. versionadded:: 3005 ++ + CLI Example: + + .. code-block:: bash +@@ -5336,6 +5343,7 @@ def check_managed( + serole=serole, + setype=setype, + serange=serange, ++ follow_symlinks=follow_symlinks, + ) + # Ignore permission for files written temporary directories + # Files in any path will still be set correctly using get_managed() +@@ -5372,6 +5380,7 @@ def check_managed_changes( + setype=None, + serange=None, + verify_ssl=True, ++ follow_symlinks=False, + **kwargs + ): + """ +@@ -5387,6 +5396,12 @@ def check_managed_changes( + + .. versionadded:: 3002 + ++ follow_symlinks ++ If the desired path is a symlink, follow it and check the permissions ++ of the file to which the symlink points. ++ ++ .. versionadded:: 3005 ++ + CLI Example: + + .. code-block:: bash +@@ -5456,6 +5471,7 @@ def check_managed_changes( + serole=serole, + setype=setype, + serange=serange, ++ follow_symlinks=follow_symlinks, + ) + __clean_tmp(sfn) + return changes +@@ -5477,6 +5493,7 @@ def check_file_meta( + setype=None, + serange=None, + verify_ssl=True, ++ follow_symlinks=False, + ): + """ + Check for the changes in the file metadata. +@@ -5553,6 +5570,12 @@ def check_file_meta( + will not attempt to validate the servers certificate. Default is True. + + .. versionadded:: 3002 ++ ++ follow_symlinks ++ If the desired path is a symlink, follow it and check the permissions ++ of the file to which the symlink points. ++ ++ .. versionadded:: 3005 + """ + changes = {} + if not source_sum: +@@ -5560,7 +5583,9 @@ def check_file_meta( + + try: + lstats = stats( +- name, hash_type=source_sum.get("hash_type", None), follow_symlinks=False ++ name, ++ hash_type=source_sum.get("hash_type", None), ++ follow_symlinks=follow_symlinks, + ) + except CommandExecutionError: + lstats = {} +diff --git a/salt/states/file.py b/salt/states/file.py +index 54e7decf86..a6288025e5 100644 +--- a/salt/states/file.py ++++ b/salt/states/file.py +@@ -3038,6 +3038,7 @@ def managed( + setype=setype, + serange=serange, + verify_ssl=verify_ssl, ++ follow_symlinks=follow_symlinks, + **kwargs + ) + +diff --git a/tests/pytests/unit/modules/file/test_file_check.py b/tests/pytests/unit/modules/file/test_file_check.py +new file mode 100644 +index 0000000000..bd0379ddae +--- /dev/null ++++ b/tests/pytests/unit/modules/file/test_file_check.py +@@ -0,0 +1,144 @@ ++import getpass ++import logging ++import os ++ ++import pytest ++import salt.modules.file as filemod ++import salt.utils.files ++import salt.utils.platform ++ ++log = logging.getLogger(__name__) ++ ++ ++@pytest.fixture ++def configure_loader_modules(): ++ return {filemod: {"__context__": {}}} ++ ++ ++@pytest.fixture ++def tfile(tmp_path): ++ filename = str(tmp_path / "file-check-test-file") ++ ++ with salt.utils.files.fopen(filename, "w") as fp: ++ fp.write("Hi hello! I am a file.") ++ os.chmod(filename, 0o644) ++ ++ yield filename ++ ++ os.remove(filename) ++ ++ ++@pytest.fixture ++def a_link(tmp_path, tfile): ++ linkname = str(tmp_path / "a_link") ++ os.symlink(tfile, linkname) ++ ++ yield linkname ++ ++ os.remove(linkname) ++ ++ ++def get_link_perms(): ++ if salt.utils.platform.is_linux(): ++ return "0777" ++ return "0755" ++ ++ ++@pytest.mark.skip_on_windows(reason="os.symlink is not available on Windows") ++def test_check_file_meta_follow_symlinks(a_link, tfile): ++ user = getpass.getuser() ++ lperms = get_link_perms() ++ ++ # follow_symlinks=False (default) ++ ret = filemod.check_file_meta( ++ a_link, tfile, None, None, user, None, lperms, None, None ++ ) ++ assert ret == {} ++ ++ ret = filemod.check_file_meta( ++ a_link, tfile, None, None, user, None, "0644", None, None ++ ) ++ assert ret == {"mode": "0644"} ++ ++ # follow_symlinks=True ++ ret = filemod.check_file_meta( ++ a_link, tfile, None, None, user, None, "0644", None, None, follow_symlinks=True ++ ) ++ assert ret == {} ++ ++ ++@pytest.mark.skip_on_windows(reason="os.symlink is not available on Windows") ++def test_check_managed_follow_symlinks(a_link, tfile): ++ user = getpass.getuser() ++ lperms = get_link_perms() ++ ++ # Function check_managed() ignores mode changes for files in the temp directory. ++ # Trick it to not recognize a_link as such. ++ a_link = "/" + a_link ++ ++ # follow_symlinks=False (default) ++ ret, comments = filemod.check_managed( ++ a_link, tfile, None, None, user, None, lperms, None, None, None, None, None ++ ) ++ assert ret is True ++ assert comments == "The file {} is in the correct state".format(a_link) ++ ++ ret, comments = filemod.check_managed( ++ a_link, tfile, None, None, user, None, "0644", None, None, None, None, None ++ ) ++ assert ret is None ++ assert comments == "The following values are set to be changed:\nmode: 0644\n" ++ ++ # follow_symlinks=True ++ ret, comments = filemod.check_managed( ++ a_link, ++ tfile, ++ None, ++ None, ++ user, ++ None, ++ "0644", ++ None, ++ None, ++ None, ++ None, ++ None, ++ follow_symlinks=True, ++ ) ++ assert ret is True ++ assert comments == "The file {} is in the correct state".format(a_link) ++ ++ ++@pytest.mark.skip_on_windows(reason="os.symlink is not available on Windows") ++def test_check_managed_changes_follow_symlinks(a_link, tfile): ++ user = getpass.getuser() ++ lperms = get_link_perms() ++ ++ # follow_symlinks=False (default) ++ ret = filemod.check_managed_changes( ++ a_link, tfile, None, None, user, None, lperms, None, None, None, None, None ++ ) ++ assert ret == {} ++ ++ ret = filemod.check_managed_changes( ++ a_link, tfile, None, None, user, None, "0644", None, None, None, None, None ++ ) ++ assert ret == {"mode": "0644"} ++ ++ # follow_symlinks=True ++ ret = filemod.check_managed_changes( ++ a_link, ++ tfile, ++ None, ++ None, ++ user, ++ None, ++ "0644", ++ None, ++ None, ++ None, ++ None, ++ None, ++ follow_symlinks=True, ++ ) ++ assert ret == {} +-- +2.36.1 + + diff --git a/ignore-erros-on-reading-license-files-with-dpkg_lowp.patch b/ignore-erros-on-reading-license-files-with-dpkg_lowp.patch new file mode 100644 index 0000000..6bba194 --- /dev/null +++ b/ignore-erros-on-reading-license-files-with-dpkg_lowp.patch @@ -0,0 +1,56 @@ +From 90d0e3ce40e46a9bff3e477b61e02cf3e87e8b9f Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Mon, 27 Jun 2022 17:55:49 +0300 +Subject: [PATCH] Ignore erros on reading license files with dpkg_lowpkg + (bsc#1197288) + +* Ignore erros on reading license files with dpkg_lowpkg (bsc#1197288) + +* Add test for license reading with dpkg_lowpkg +--- + salt/modules/dpkg_lowpkg.py | 2 +- + tests/pytests/unit/modules/test_dpkg_lowpkg.py | 18 ++++++++++++++++++ + 2 files changed, 19 insertions(+), 1 deletion(-) + create mode 100644 tests/pytests/unit/modules/test_dpkg_lowpkg.py + +diff --git a/salt/modules/dpkg_lowpkg.py b/salt/modules/dpkg_lowpkg.py +index afbd619490..2c25b1fb2a 100644 +--- a/salt/modules/dpkg_lowpkg.py ++++ b/salt/modules/dpkg_lowpkg.py +@@ -361,7 +361,7 @@ def _get_pkg_license(pkg): + licenses = set() + cpr = "/usr/share/doc/{}/copyright".format(pkg) + if os.path.exists(cpr): +- with salt.utils.files.fopen(cpr) as fp_: ++ with salt.utils.files.fopen(cpr, errors="ignore") as fp_: + for line in salt.utils.stringutils.to_unicode(fp_.read()).split(os.linesep): + if line.startswith("License:"): + licenses.add(line.split(":", 1)[1].strip()) +diff --git a/tests/pytests/unit/modules/test_dpkg_lowpkg.py b/tests/pytests/unit/modules/test_dpkg_lowpkg.py +new file mode 100644 +index 0000000000..1a89660c02 +--- /dev/null ++++ b/tests/pytests/unit/modules/test_dpkg_lowpkg.py +@@ -0,0 +1,18 @@ ++import os ++ ++import salt.modules.dpkg_lowpkg as dpkg ++from tests.support.mock import MagicMock, mock_open, patch ++ ++ ++def test_get_pkg_license(): ++ """ ++ Test _get_pkg_license for ignore errors on reading license from copyright files ++ """ ++ license_read_mock = mock_open(read_data="") ++ with patch.object(os.path, "exists", MagicMock(return_value=True)), patch( ++ "salt.utils.files.fopen", license_read_mock ++ ): ++ dpkg._get_pkg_license("bash") ++ ++ assert license_read_mock.calls[0].args[0] == "/usr/share/doc/bash/copyright" ++ assert license_read_mock.calls[0].kwargs["errors"] == "ignore" +-- +2.36.1 + + diff --git a/normalize-package-names-once-with-pkg.installed-remo.patch b/normalize-package-names-once-with-pkg.installed-remo.patch new file mode 100644 index 0000000..144af20 --- /dev/null +++ b/normalize-package-names-once-with-pkg.installed-remo.patch @@ -0,0 +1,296 @@ +From 09afcd0d04788ec4351c1c0b73a0c6eb3b0fd8c9 Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Mon, 27 Jun 2022 18:01:21 +0300 +Subject: [PATCH] Normalize package names once with pkg.installed/removed + using yum (bsc#1195895) + +* Normalize the package names only once on install/remove + +* Add test for checking pkg.installed/removed with only normalisation + +* Fix split_arch conditions + +* Fix test_pkg +--- + salt/modules/yumpkg.py | 18 ++- + salt/states/pkg.py | 3 +- + tests/pytests/unit/states/test_pkg.py | 177 +++++++++++++++++++++++++- + 3 files changed, 192 insertions(+), 6 deletions(-) + +diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py +index 9f8f548e5f..3138ac2e59 100644 +--- a/salt/modules/yumpkg.py ++++ b/salt/modules/yumpkg.py +@@ -1449,7 +1449,12 @@ def install( + + try: + pkg_params, pkg_type = __salt__["pkg_resource.parse_targets"]( +- name, pkgs, sources, saltenv=saltenv, normalize=normalize, **kwargs ++ name, ++ pkgs, ++ sources, ++ saltenv=saltenv, ++ normalize=normalize and kwargs.get("split_arch", True), ++ **kwargs + ) + except MinionError as exc: + raise CommandExecutionError(exc) +@@ -1603,7 +1608,10 @@ def install( + except ValueError: + pass + else: +- if archpart in salt.utils.pkg.rpm.ARCHES: ++ if archpart in salt.utils.pkg.rpm.ARCHES and ( ++ archpart != __grains__["osarch"] ++ or kwargs.get("split_arch", True) ++ ): + arch = "." + archpart + pkgname = namepart + +@@ -2134,11 +2142,13 @@ def remove(name=None, pkgs=None, **kwargs): # pylint: disable=W0613 + arch = "" + pkgname = target + try: +- namepart, archpart = target.rsplit(".", 1) ++ namepart, archpart = pkgname.rsplit(".", 1) + except ValueError: + pass + else: +- if archpart in salt.utils.pkg.rpm.ARCHES: ++ if archpart in salt.utils.pkg.rpm.ARCHES and ( ++ archpart != __grains__["osarch"] or kwargs.get("split_arch", True) ++ ): + arch = "." + archpart + pkgname = namepart + # Since we don't always have the arch info, epoch information has to parsed out. But +diff --git a/salt/states/pkg.py b/salt/states/pkg.py +index 0d601e1aaf..71298e6c7a 100644 +--- a/salt/states/pkg.py ++++ b/salt/states/pkg.py +@@ -1901,6 +1901,7 @@ def installed( + normalize=normalize, + update_holds=update_holds, + ignore_epoch=ignore_epoch, ++ split_arch=False, + **kwargs + ) + except CommandExecutionError as exc: +@@ -2973,7 +2974,7 @@ def _uninstall( + } + + changes = __salt__["pkg.{}".format(action)]( +- name, pkgs=pkgs, version=version, **kwargs ++ name, pkgs=pkgs, version=version, split_arch=False, **kwargs + ) + new = __salt__["pkg.list_pkgs"](versions_as_list=True, **kwargs) + failed = [] +diff --git a/tests/pytests/unit/states/test_pkg.py b/tests/pytests/unit/states/test_pkg.py +index 17b91bcb39..10acae9f88 100644 +--- a/tests/pytests/unit/states/test_pkg.py ++++ b/tests/pytests/unit/states/test_pkg.py +@@ -2,6 +2,8 @@ import logging + + import pytest + import salt.modules.beacons as beaconmod ++import salt.modules.pkg_resource as pkg_resource ++import salt.modules.yumpkg as yumpkg + import salt.states.beacon as beaconstate + import salt.states.pkg as pkg + import salt.utils.state as state_utils +@@ -17,7 +19,7 @@ def configure_loader_modules(): + pkg: { + "__env__": "base", + "__salt__": {}, +- "__grains__": {"os": "CentOS"}, ++ "__grains__": {"os": "CentOS", "os_family": "RedHat"}, + "__opts__": {"test": False, "cachedir": ""}, + "__instance_id__": "", + "__low__": {}, +@@ -25,6 +27,15 @@ def configure_loader_modules(): + }, + beaconstate: {"__salt__": {}, "__opts__": {}}, + beaconmod: {"__salt__": {}, "__opts__": {}}, ++ pkg_resource: { ++ "__salt__": {}, ++ "__grains__": {"os": "CentOS", "os_family": "RedHat"}, ++ }, ++ yumpkg: { ++ "__salt__": {}, ++ "__grains__": {"osarch": "x86_64", "osmajorrelease": 7}, ++ "__opts__": {}, ++ }, + } + + +@@ -715,3 +726,167 @@ def test_held_unheld(package_manager): + hold_mock.assert_not_called() + unhold_mock.assert_any_call(name="held-test", pkgs=["baz"]) + unhold_mock.assert_any_call(name="held-test", pkgs=["bar"]) ++ ++ ++def test_installed_with_single_normalize(): ++ """ ++ Test pkg.installed with preventing multiple package name normalisation ++ """ ++ ++ list_no_weird_installed = { ++ "pkga": "1.0.1", ++ "pkgb": "1.0.2", ++ "pkgc": "1.0.3", ++ } ++ list_no_weird_installed_ver_list = { ++ "pkga": ["1.0.1"], ++ "pkgb": ["1.0.2"], ++ "pkgc": ["1.0.3"], ++ } ++ list_with_weird_installed = { ++ "pkga": "1.0.1", ++ "pkgb": "1.0.2", ++ "pkgc": "1.0.3", ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": "20220214-2.1", ++ } ++ list_with_weird_installed_ver_list = { ++ "pkga": ["1.0.1"], ++ "pkgb": ["1.0.2"], ++ "pkgc": ["1.0.3"], ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": ["20220214-2.1"], ++ } ++ list_pkgs = MagicMock( ++ side_effect=[ ++ # For the package with version specified ++ list_no_weird_installed_ver_list, ++ {}, ++ list_no_weird_installed, ++ list_no_weird_installed_ver_list, ++ list_with_weird_installed, ++ list_with_weird_installed_ver_list, ++ # For the package with no version specified ++ list_no_weird_installed_ver_list, ++ {}, ++ list_no_weird_installed, ++ list_no_weird_installed_ver_list, ++ list_with_weird_installed, ++ list_with_weird_installed_ver_list, ++ ] ++ ) ++ ++ salt_dict = { ++ "pkg.install": yumpkg.install, ++ "pkg.list_pkgs": list_pkgs, ++ "pkg.normalize_name": yumpkg.normalize_name, ++ "pkg_resource.version_clean": pkg_resource.version_clean, ++ "pkg_resource.parse_targets": pkg_resource.parse_targets, ++ } ++ ++ with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch( ++ "salt.modules.yumpkg.version_cmp", MagicMock(return_value=0) ++ ), patch( ++ "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) ++ ) as call_yum_mock, patch.dict( ++ pkg.__salt__, salt_dict ++ ), patch.dict( ++ pkg_resource.__salt__, salt_dict ++ ), patch.dict( ++ yumpkg.__salt__, salt_dict ++ ), patch.dict( ++ yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7} ++ ), patch.object( ++ yumpkg, "list_holds", MagicMock() ++ ): ++ ++ expected = { ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": { ++ "old": "", ++ "new": "20220214-2.1", ++ } ++ } ++ ret = pkg.installed( ++ "test_install", ++ pkgs=[{"weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch": "20220214-2.1"}], ++ ) ++ call_yum_mock.assert_called_once() ++ assert ( ++ call_yum_mock.mock_calls[0].args[0][2] ++ == "weird-name-1.2.3-1234.5.6.test7tst.x86_64-20220214-2.1" ++ ) ++ assert ret["result"] ++ assert ret["changes"] == expected ++ ++ ++def test_removed_with_single_normalize(): ++ """ ++ Test pkg.removed with preventing multiple package name normalisation ++ """ ++ ++ list_no_weird_installed = { ++ "pkga": "1.0.1", ++ "pkgb": "1.0.2", ++ "pkgc": "1.0.3", ++ } ++ list_no_weird_installed_ver_list = { ++ "pkga": ["1.0.1"], ++ "pkgb": ["1.0.2"], ++ "pkgc": ["1.0.3"], ++ } ++ list_with_weird_installed = { ++ "pkga": "1.0.1", ++ "pkgb": "1.0.2", ++ "pkgc": "1.0.3", ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": "20220214-2.1", ++ } ++ list_with_weird_installed_ver_list = { ++ "pkga": ["1.0.1"], ++ "pkgb": ["1.0.2"], ++ "pkgc": ["1.0.3"], ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": ["20220214-2.1"], ++ } ++ list_pkgs = MagicMock( ++ side_effect=[ ++ list_with_weird_installed_ver_list, ++ list_with_weird_installed, ++ list_no_weird_installed, ++ list_no_weird_installed_ver_list, ++ ] ++ ) ++ ++ salt_dict = { ++ "pkg.remove": yumpkg.remove, ++ "pkg.list_pkgs": list_pkgs, ++ "pkg.normalize_name": yumpkg.normalize_name, ++ "pkg_resource.parse_targets": pkg_resource.parse_targets, ++ "pkg_resource.version_clean": pkg_resource.version_clean, ++ } ++ ++ with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch( ++ "salt.modules.yumpkg.version_cmp", MagicMock(return_value=0) ++ ), patch( ++ "salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0}) ++ ) as call_yum_mock, patch.dict( ++ pkg.__salt__, salt_dict ++ ), patch.dict( ++ pkg_resource.__salt__, salt_dict ++ ), patch.dict( ++ yumpkg.__salt__, salt_dict ++ ): ++ ++ expected = { ++ "weird-name-1.2.3-1234.5.6.test7tst.x86_64": { ++ "old": "20220214-2.1", ++ "new": "", ++ } ++ } ++ ret = pkg.removed( ++ "test_remove", ++ pkgs=[{"weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch": "20220214-2.1"}], ++ ) ++ call_yum_mock.assert_called_once() ++ assert ( ++ call_yum_mock.mock_calls[0].args[0][2] ++ == "weird-name-1.2.3-1234.5.6.test7tst.x86_64-20220214-2.1" ++ ) ++ assert ret["result"] ++ assert ret["changes"] == expected +-- +2.36.1 + + diff --git a/salt.changes b/salt.changes index 6868b08..5007feb 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,44 @@ +------------------------------------------------------------------- +Fri Jul 8 09:45:54 UTC 2022 - Pablo Suárez Hernández + +- Add support for gpgautoimport in zypperpkg module +- Update Salt to work with Jinja >= and <= 3.1.0 (bsc#1198744) +- Fix salt.states.file.managed() for follow_symlinks=True and test=True (bsc#1199372) +- Make Salt 3004 compatible with pyzmq >= 23.0.0 (bsc#1201082) + +- Added: + * fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch + * add-support-for-gpgautoimport-539.patch + * fix-62092-catch-zmq.error.zmqerror-to-set-hwm-for-zm.patch + * fix-salt.states.file.managed-for-follow_symlinks-tru.patch + +------------------------------------------------------------------- +Thu Jul 7 14:58:25 UTC 2022 - Pablo Suárez Hernández + +- Add support for name, pkgs and diff_attr parameters to upgrade + function for zypper and yum (bsc#1198489) + +- Added: + * add-support-for-name-pkgs-and-diff_attr-parameters-t.patch + +------------------------------------------------------------------- +Tue Jun 28 07:40:48 UTC 2022 - Victor Zhestkov + +- Fix ownership of salt thin directory when using the Salt Bundle +- Set default target for pip from VENV_PIP_TARGET environment variable +- Normalize package names once with pkg.installed/removed using yum (bsc#1195895) +- Save log to logfile with docker.build +- Use Salt Bundle in dockermod +- Ignore erros on reading license files with dpkg_lowpkg (bsc#1197288) + +- Added: + * normalize-package-names-once-with-pkg.installed-remo.patch + * use-salt-bundle-in-dockermod.patch + * fix-ownership-of-salt-thin-directory-when-using-the-.patch + * ignore-erros-on-reading-license-files-with-dpkg_lowp.patch + * set-default-target-for-pip-from-venv_pip_target-envi.patch + * save-log-to-logfile-with-docker.build.patch + ------------------------------------------------------------------- Thu Jun 16 09:52:06 UTC 2022 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index f2ee287..f1c9331 100644 --- a/salt.spec +++ b/salt.spec @@ -308,7 +308,28 @@ Patch79: fix-regression-with-depending-client.ssh-on-psutil-b.patch Patch80: make-sure-saltcacheloader-use-correct-fileclient-519.patch # PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/commit/e068a34ccb2e17ae7224f8016a24b727f726d4c8 Patch81: fix-for-cve-2022-22967-bsc-1200566.patch - +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61827 +Patch82: ignore-erros-on-reading-license-files-with-dpkg_lowp.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62109 +Patch83: use-salt-bundle-in-dockermod.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61984 +Patch84: save-log-to-logfile-with-docker.build.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62029 +Patch85: normalize-package-names-once-with-pkg.installed-remo.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62089 +Patch86: set-default-target-for-pip-from-venv_pip_target-envi.patch +# PATCH-FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/534 +Patch87: fix-ownership-of-salt-thin-directory-when-using-the-.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62033 +Patch88: add-support-for-name-pkgs-and-diff_attr-parameters-t.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62119 +Patch89: fix-62092-catch-zmq.error.zmqerror-to-set-hwm-for-zm.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62067 +Patch90: fix-salt.states.file.managed-for-follow_symlinks-tru.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/61856 +Patch91: fix-jinja2-contextfuntion-base-on-version-bsc-119874.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/62209 +Patch92: add-support-for-gpgautoimport-539.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate diff --git a/save-log-to-logfile-with-docker.build.patch b/save-log-to-logfile-with-docker.build.patch new file mode 100644 index 0000000..9061fc3 --- /dev/null +++ b/save-log-to-logfile-with-docker.build.patch @@ -0,0 +1,56 @@ +From c70db2e50599339118c9bf00c69f5cd38ef220bb Mon Sep 17 00:00:00 2001 +From: Vladimir Nadvornik +Date: Mon, 27 Jun 2022 17:00:58 +0200 +Subject: [PATCH] Save log to logfile with docker.build + +--- + salt/modules/dockermod.py | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py +index e6b81e7f09..1f871b40cf 100644 +--- a/salt/modules/dockermod.py ++++ b/salt/modules/dockermod.py +@@ -3990,6 +3990,7 @@ def build( + fileobj=None, + dockerfile=None, + buildargs=None, ++ logfile=None, + ): + """ + .. versionchanged:: 2018.3.0 +@@ -4043,6 +4044,9 @@ def build( + buildargs + A dictionary of build arguments provided to the docker build process. + ++ logfile ++ Path to log file. Output from build is written to this file if not None. ++ + + **RETURN DATA** + +@@ -4117,6 +4121,20 @@ def build( + stream_data = [] + for line in response: + stream_data.extend(salt.utils.json.loads(line, cls=DockerJSONDecoder)) ++ ++ if logfile: ++ try: ++ with salt.utils.files.fopen(logfile, "a") as f: ++ for item in stream_data: ++ try: ++ item_type = next(iter(item)) ++ except StopIteration: ++ continue ++ if item_type == "stream": ++ f.write(item[item_type]) ++ except OSError: ++ log.error("Unable to write logfile '%s'", logfile) ++ + errors = [] + # Iterate through API response and collect information + for item in stream_data: +-- +2.36.1 + + diff --git a/set-default-target-for-pip-from-venv_pip_target-envi.patch b/set-default-target-for-pip-from-venv_pip_target-envi.patch new file mode 100644 index 0000000..a82374f --- /dev/null +++ b/set-default-target-for-pip-from-venv_pip_target-envi.patch @@ -0,0 +1,1861 @@ +From 003266fc86c1364a41ac4bd35207290c036151a0 Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Mon, 27 Jun 2022 18:02:31 +0300 +Subject: [PATCH] Set default target for pip from VENV_PIP_TARGET + environment variable + +* Use VENV_PIP_TARGET as a target for pkg.install + +if set and no target specified on the call + +* Add test for VENV_PIP_TARGET environment variable + +* Changelog entry +--- + changelog/62089.changed | 1 + + salt/modules/pip.py | 6 + + tests/pytests/unit/modules/test_pip.py | 1806 ++++++++++++++++++++++++ + 3 files changed, 1813 insertions(+) + create mode 100644 changelog/62089.changed + create mode 100644 tests/pytests/unit/modules/test_pip.py + +diff --git a/changelog/62089.changed b/changelog/62089.changed +new file mode 100644 +index 0000000000..09feb2e922 +--- /dev/null ++++ b/changelog/62089.changed +@@ -0,0 +1 @@ ++Use VENV_PIP_TARGET environment variable as a default target for pip if present. +diff --git a/salt/modules/pip.py b/salt/modules/pip.py +index da26416662..9410024fd5 100644 +--- a/salt/modules/pip.py ++++ b/salt/modules/pip.py +@@ -858,6 +858,12 @@ def install( + if build: + cmd.extend(["--build", build]) + ++ # Use VENV_PIP_TARGET environment variable value as target ++ # if set and no target specified on the function call ++ target_env = os.environ.get("VENV_PIP_TARGET", None) ++ if target is None and target_env is not None: ++ target = target_env ++ + if target: + cmd.extend(["--target", target]) + +diff --git a/tests/pytests/unit/modules/test_pip.py b/tests/pytests/unit/modules/test_pip.py +new file mode 100644 +index 0000000000..ae9005d806 +--- /dev/null ++++ b/tests/pytests/unit/modules/test_pip.py +@@ -0,0 +1,1806 @@ ++import os ++import sys ++ ++import pytest ++import salt.modules.pip as pip ++import salt.utils.files ++import salt.utils.platform ++from salt.exceptions import CommandExecutionError ++from tests.support.mock import MagicMock, patch ++ ++ ++class FakeFopen: ++ def __init__(self, filename): ++ d = { ++ "requirements-0.txt": ( ++ b"--index-url http://fake.com/simple\n\n" ++ b"one # -r wrong.txt, other\n" ++ b"two # --requirement wrong.exe;some\n" ++ b"three\n" ++ b"-r requirements-1.txt\n" ++ b"# nothing\n" ++ ), ++ "requirements-1.txt": ( ++ "four\n" ++ "five\n" ++ "--requirement=requirements-2.txt\t# --requirements-2.txt\n\n" ++ ), ++ "requirements-2.txt": b"""six""", ++ "requirements-3.txt": ( ++ b"# some comment\n" ++ b"-e git+ssh://git.example.com/MyProject#egg=MyProject # the project\n" ++ b"seven\n" ++ b"-e git+ssh://git.example.com/Example#egg=example\n" ++ b"eight # -e something#or other\n" ++ b"--requirement requirements-4.txt\n\n" ++ ), ++ "requirements-4.txt": "", ++ } ++ self.val = d[filename] ++ ++ def __enter__(self): ++ return self ++ ++ def __exit__(self, *args, **kwargs): ++ pass ++ ++ def read(self): ++ return self.val ++ ++ ++@pytest.fixture ++def expected_user(): ++ return "fnord" ++ ++ ++@pytest.fixture ++def configure_loader_modules(): ++ return {pip: {"__salt__": {"cmd.which_bin": lambda _: "pip"}}} ++ ++ ++def test__pip_bin_env(): ++ ret = pip._pip_bin_env(None, "C:/Users/ch44d/Documents/salt/tests/pip.exe") ++ if salt.utils.platform.is_windows(): ++ assert ret == "C:/Users/ch44d/Documents/salt/tests" ++ else: ++ assert ret is None ++ ++ ++def test__pip_bin_env_no_change(): ++ cwd = "C:/Users/ch44d/Desktop" ++ ret = pip._pip_bin_env(cwd, "C:/Users/ch44d/Documents/salt/tests/pip.exe") ++ assert ret == cwd ++ ++ ++def test__pip_bin_env_no_bin_env(): ++ ret = pip._pip_bin_env(None, None) ++ assert ret is None ++ ++ ++def test_install_frozen_app(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch("sys.frozen", True, create=True): ++ with patch("sys._MEIPASS", True, create=True): ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg) ++ expected = [ ++ sys.executable, ++ "pip", ++ "install", ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ python_shell=False, ++ saltenv="base", ++ use_vt=False, ++ runas=None, ++ ) ++ ++ ++def test_install_source_app(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch("sys.frozen", False, create=True): ++ with patch("sys._MEIPASS", False, create=True): ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ python_shell=False, ++ saltenv="base", ++ use_vt=False, ++ runas=None, ++ ) ++ ++ ++def test_fix4361(): ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(requirements="requirements.txt") ++ expected_cmd = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--requirement", ++ "requirements.txt", ++ ] ++ mock.assert_called_with( ++ expected_cmd, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_editable_without_egg_fails(): ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises( ++ CommandExecutionError, ++ pip.install, ++ editable="git+https://github.com/saltstack/salt-testing.git", ++ ) ++ ++ ++def test_install_multiple_editable(): ++ editables = [ ++ "git+https://github.com/jek/blinker.git#egg=Blinker", ++ "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting", ++ ] ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ for item in editables: ++ expected.extend(["--editable", item]) ++ ++ # Passing editables as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(editable=editables) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing editables as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(editable=",".join(editables)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_multiple_pkgs_and_editables(): ++ pkgs = ["pep8", "salt"] ++ editables = [ ++ "git+https://github.com/jek/blinker.git#egg=Blinker", ++ "git+https://github.com/saltstack/salt-testing.git#egg=SaltTesting", ++ ] ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ expected.extend(pkgs) ++ for item in editables: ++ expected.extend(["--editable", item]) ++ ++ # Passing editables as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=pkgs, editable=editables) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing editables as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=",".join(pkgs), editable=",".join(editables)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # As single string (just use the first element from pkgs and editables) ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=pkgs[0], editable=editables[0]) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ pkgs[0], ++ "--editable", ++ editables[0], ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_issue5940_install_multiple_pip_mirrors(): ++ """ ++ test multiple pip mirrors. This test only works with pip < 7.0.0 ++ """ ++ with patch.object(pip, "version", MagicMock(return_value="1.4")): ++ mirrors = [ ++ "http://g.pypi.python.org", ++ "http://c.pypi.python.org", ++ "http://pypi.crate.io", ++ ] ++ ++ expected = [sys.executable, "-m", "pip", "install", "--use-mirrors"] ++ for item in mirrors: ++ expected.extend(["--mirrors", item]) ++ expected.append("pep8") ++ ++ # Passing mirrors as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=["pep8"], mirrors=mirrors) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=["pep8"], mirrors=",".join(mirrors)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--use-mirrors", ++ "--mirrors", ++ mirrors[0], ++ "pep8", ++ ] ++ ++ # As single string (just use the first element from mirrors) ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkgs=["pep8"], mirrors=mirrors[0]) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_with_multiple_find_links(): ++ find_links = [ ++ "http://g.pypi.python.org", ++ "http://c.pypi.python.org", ++ "http://pypi.crate.io", ++ ] ++ pkg = "pep8" ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ for item in find_links: ++ expected.extend(["--find-links", item]) ++ expected.append(pkg) ++ ++ # Passing mirrors as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, find_links=find_links) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, find_links=",".join(find_links)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Valid protos work? ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, find_links=find_links) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--find-links", ++ find_links[0], ++ pkg, ++ ] ++ ++ # As single string (just use the first element from find_links) ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, find_links=find_links[0]) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Invalid proto raises exception ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises( ++ CommandExecutionError, ++ pip.install, ++ "'" + pkg + "'", ++ find_links="sftp://pypi.crate.io", ++ ) ++ ++ ++def test_install_no_index_with_index_url_or_extra_index_url_raises(): ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises( ++ CommandExecutionError, ++ pip.install, ++ no_index=True, ++ index_url="http://foo.tld", ++ ) ++ ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises( ++ CommandExecutionError, ++ pip.install, ++ no_index=True, ++ extra_index_url="http://foo.tld", ++ ) ++ ++ ++def test_install_failed_cached_requirements(): ++ with patch("salt.modules.pip._get_cached_requirements") as get_cached_requirements: ++ get_cached_requirements.return_value = False ++ ret = pip.install(requirements="salt://my_test_reqs") ++ assert False is ret["result"] ++ assert "my_test_reqs" in ret["comment"] ++ ++ ++def test_install_cached_requirements_used(): ++ with patch("salt.modules.pip._get_cached_requirements") as get_cached_requirements: ++ get_cached_requirements.return_value = "my_cached_reqs" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(requirements="salt://requirements.txt") ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--requirement", ++ "my_cached_reqs", ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_venv(): ++ with patch("os.path") as mock_path: ++ ++ def join(*args): ++ return os.path.normpath(os.sep.join(args)) ++ ++ mock_path.is_file.return_value = True ++ mock_path.isdir.return_value = True ++ mock_path.join = join ++ ++ if salt.utils.platform.is_windows(): ++ venv_path = "C:\\test_env" ++ bin_path = os.path.join(venv_path, "python.exe") ++ else: ++ venv_path = "/test_env" ++ bin_path = os.path.join(venv_path, "python") ++ ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ pip_bin = MagicMock(return_value=[bin_path, "-m", "pip"]) ++ ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}), patch.object( ++ pip, "_get_pip_bin", pip_bin ++ ): ++ pip.install("mock", bin_env=venv_path) ++ mock.assert_called_with( ++ [bin_path, "-m", "pip", "install", "mock"], ++ env={"VIRTUAL_ENV": venv_path}, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_log_argument_in_resulting_command(): ++ with patch("os.access") as mock_path: ++ pkg = "pep8" ++ log_path = "/tmp/pip-install.log" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, log=log_path) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--log", ++ log_path, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_non_writeable_log(): ++ with patch("os.path") as mock_path: ++ # Let's fake a non-writable log file ++ pkg = "pep8" ++ log_path = "/tmp/pip-install.log" ++ mock_path.exists.side_effect = IOError("Fooo!") ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises(IOError, pip.install, pkg, log=log_path) ++ ++ ++def test_install_timeout_argument_in_resulting_command(): ++ # Passing an int ++ pkg = "pep8" ++ expected = [sys.executable, "-m", "pip", "install", "--timeout"] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, timeout=10) ++ mock.assert_called_with( ++ expected + [10, pkg], ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing an int as a string ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, timeout="10") ++ mock.assert_called_with( ++ expected + ["10", pkg], ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing a non-int to timeout ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises(ValueError, pip.install, pkg, timeout="a") ++ ++ ++def test_install_index_url_argument_in_resulting_command(): ++ pkg = "pep8" ++ index_url = "http://foo.tld" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, index_url=index_url) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--index-url", ++ index_url, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_extra_index_url_argument_in_resulting_command(): ++ pkg = "pep8" ++ extra_index_url = "http://foo.tld" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, extra_index_url=extra_index_url) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--extra-index-url", ++ extra_index_url, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_no_index_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, no_index=True) ++ expected = [sys.executable, "-m", "pip", "install", "--no-index", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_build_argument_in_resulting_command(): ++ pkg = "pep8" ++ build = "/tmp/foo" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, build=build) ++ expected = [sys.executable, "-m", "pip", "install", "--build", build, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_target_argument_in_resulting_command(): ++ pkg = "pep8" ++ target = "/tmp/foo" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, target=target) ++ expected = [sys.executable, "-m", "pip", "install", "--target", target, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_download_argument_in_resulting_command(): ++ pkg = "pep8" ++ download = "/tmp/foo" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, download=download) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--download", ++ download, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_no_download_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, no_download=True) ++ expected = [sys.executable, "-m", "pip", "install", "--no-download", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_download_cache_dir_arguments_in_resulting_command(): ++ pkg = "pep8" ++ cache_dir_arg_mapping = { ++ "1.5.6": "--download-cache", ++ "6.0": "--cache-dir", ++ } ++ download_cache = "/tmp/foo" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ for pip_version, cmd_arg in cache_dir_arg_mapping.items(): ++ with patch("salt.modules.pip.version", MagicMock(return_value=pip_version)): ++ # test `download_cache` kwarg ++ pip.install(pkg, download_cache="/tmp/foo") ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ cmd_arg, ++ download_cache, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # test `cache_dir` kwarg ++ pip.install(pkg, cache_dir="/tmp/foo") ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_source_argument_in_resulting_command(): ++ pkg = "pep8" ++ source = "/tmp/foo" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, source=source) ++ expected = [sys.executable, "-m", "pip", "install", "--source", source, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_exists_action_argument_in_resulting_command(): ++ pkg = "pep8" ++ for action in ("s", "i", "w", "b"): ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, exists_action=action) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--exists-action", ++ action, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Test for invalid action ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises(CommandExecutionError, pip.install, pkg, exists_action="d") ++ ++ ++def test_install_install_options_argument_in_resulting_command(): ++ install_options = ["--exec-prefix=/foo/bar", "--install-scripts=/foo/bar/bin"] ++ pkg = "pep8" ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ for item in install_options: ++ expected.extend(["--install-option", item]) ++ expected.append(pkg) ++ ++ # Passing options as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, install_options=install_options) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, install_options=",".join(install_options)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a single string entry ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, install_options=install_options[0]) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--install-option", ++ install_options[0], ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_global_options_argument_in_resulting_command(): ++ global_options = ["--quiet", "--no-user-cfg"] ++ pkg = "pep8" ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ for item in global_options: ++ expected.extend(["--global-option", item]) ++ expected.append(pkg) ++ ++ # Passing options as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, global_options=global_options) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a comma separated list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, global_options=",".join(global_options)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing mirrors as a single string entry ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, global_options=global_options[0]) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--global-option", ++ global_options[0], ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_upgrade_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, upgrade=True) ++ expected = [sys.executable, "-m", "pip", "install", "--upgrade", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_force_reinstall_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, force_reinstall=True) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--force-reinstall", ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_ignore_installed_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, ignore_installed=True) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--ignore-installed", ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_no_deps_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, no_deps=True) ++ expected = [sys.executable, "-m", "pip", "install", "--no-deps", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_no_install_argument_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, no_install=True) ++ expected = [sys.executable, "-m", "pip", "install", "--no-install", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_proxy_argument_in_resulting_command(): ++ pkg = "pep8" ++ proxy = "salt-user:salt-passwd@salt-proxy:3128" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(pkg, proxy=proxy) ++ expected = [sys.executable, "-m", "pip", "install", "--proxy", proxy, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_proxy_false_argument_in_resulting_command(): ++ """ ++ Checking that there is no proxy set if proxy arg is set to False ++ even if the global proxy is set. ++ """ ++ pkg = "pep8" ++ proxy = False ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ config_mock = { ++ "proxy_host": "salt-proxy", ++ "proxy_port": "3128", ++ "proxy_username": "salt-user", ++ "proxy_password": "salt-passwd", ++ } ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch.dict(pip.__opts__, config_mock): ++ pip.install(pkg, proxy=proxy) ++ expected = [sys.executable, "-m", "pip", "install", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_global_proxy_in_resulting_command(): ++ """ ++ Checking that there is proxy set if global proxy is set. ++ """ ++ pkg = "pep8" ++ proxy = "http://salt-user:salt-passwd@salt-proxy:3128" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ config_mock = { ++ "proxy_host": "salt-proxy", ++ "proxy_port": "3128", ++ "proxy_username": "salt-user", ++ "proxy_password": "salt-passwd", ++ } ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch.dict(pip.__opts__, config_mock): ++ pip.install(pkg) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--proxy", ++ proxy, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_multiple_requirements_arguments_in_resulting_command(): ++ with patch("salt.modules.pip._get_cached_requirements") as get_cached_requirements: ++ cached_reqs = ["my_cached_reqs-1", "my_cached_reqs-2"] ++ get_cached_requirements.side_effect = cached_reqs ++ requirements = ["salt://requirements-1.txt", "salt://requirements-2.txt"] ++ ++ expected = [sys.executable, "-m", "pip", "install"] ++ for item in cached_reqs: ++ expected.extend(["--requirement", item]) ++ ++ # Passing option as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(requirements=requirements) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing option as a comma separated list ++ get_cached_requirements.side_effect = cached_reqs ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(requirements=",".join(requirements)) ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing option as a single string entry ++ get_cached_requirements.side_effect = [cached_reqs[0]] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install(requirements=requirements[0]) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ "--requirement", ++ cached_reqs[0], ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_extra_args_arguments_in_resulting_command(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.install( ++ pkg, extra_args=[{"--latest-pip-kwarg": "param"}, "--latest-pip-arg"] ++ ) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "install", ++ pkg, ++ "--latest-pip-kwarg", ++ "param", ++ "--latest-pip-arg", ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_install_extra_args_arguments_recursion_error(): ++ pkg = "pep8" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ ++ pytest.raises( ++ TypeError, ++ lambda: pip.install( ++ pkg, extra_args=[{"--latest-pip-kwarg": ["param1", "param2"]}] ++ ), ++ ) ++ ++ pytest.raises( ++ TypeError, ++ lambda: pip.install( ++ pkg, extra_args=[{"--latest-pip-kwarg": [{"--too-deep": dict()}]}] ++ ), ++ ) ++ ++ ++def test_uninstall_multiple_requirements_arguments_in_resulting_command(): ++ with patch("salt.modules.pip._get_cached_requirements") as get_cached_requirements: ++ cached_reqs = ["my_cached_reqs-1", "my_cached_reqs-2"] ++ get_cached_requirements.side_effect = cached_reqs ++ requirements = ["salt://requirements-1.txt", "salt://requirements-2.txt"] ++ ++ expected = [sys.executable, "-m", "pip", "uninstall", "-y"] ++ for item in cached_reqs: ++ expected.extend(["--requirement", item]) ++ ++ # Passing option as a list ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(requirements=requirements) ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing option as a comma separated list ++ get_cached_requirements.side_effect = cached_reqs ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(requirements=",".join(requirements)) ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing option as a single string entry ++ get_cached_requirements.side_effect = [cached_reqs[0]] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(requirements=requirements[0]) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "uninstall", ++ "-y", ++ "--requirement", ++ cached_reqs[0], ++ ] ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_uninstall_global_proxy_in_resulting_command(): ++ """ ++ Checking that there is proxy set if global proxy is set. ++ """ ++ pkg = "pep8" ++ proxy = "http://salt-user:salt-passwd@salt-proxy:3128" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ config_mock = { ++ "proxy_host": "salt-proxy", ++ "proxy_port": "3128", ++ "proxy_username": "salt-user", ++ "proxy_password": "salt-passwd", ++ } ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch.dict(pip.__opts__, config_mock): ++ pip.uninstall(pkg) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "uninstall", ++ "-y", ++ "--proxy", ++ proxy, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_uninstall_proxy_false_argument_in_resulting_command(): ++ """ ++ Checking that there is no proxy set if proxy arg is set to False ++ even if the global proxy is set. ++ """ ++ pkg = "pep8" ++ proxy = False ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ config_mock = { ++ "proxy_host": "salt-proxy", ++ "proxy_port": "3128", ++ "proxy_username": "salt-user", ++ "proxy_password": "salt-passwd", ++ } ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch.dict(pip.__opts__, config_mock): ++ pip.uninstall(pkg, proxy=proxy) ++ expected = [sys.executable, "-m", "pip", "uninstall", "-y", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_uninstall_log_argument_in_resulting_command(): ++ pkg = "pep8" ++ log_path = "/tmp/pip-install.log" ++ ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(pkg, log=log_path) ++ expected = [ ++ sys.executable, ++ "-m", ++ "pip", ++ "uninstall", ++ "-y", ++ "--log", ++ log_path, ++ pkg, ++ ] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Let's fake a non-writable log file ++ with patch("os.path") as mock_path: ++ mock_path.exists.side_effect = IOError("Fooo!") ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises(IOError, pip.uninstall, pkg, log=log_path) ++ ++ ++def test_uninstall_timeout_argument_in_resulting_command(): ++ pkg = "pep8" ++ expected = [sys.executable, "-m", "pip", "uninstall", "-y", "--timeout"] ++ # Passing an int ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(pkg, timeout=10) ++ mock.assert_called_with( ++ expected + [10, pkg], ++ cwd=None, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing an int as a string ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pip.uninstall(pkg, timeout="10") ++ mock.assert_called_with( ++ expected + ["10", pkg], ++ cwd=None, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ # Passing a non-int to timeout ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ pytest.raises(ValueError, pip.uninstall, pkg, timeout="a") ++ ++ ++def test_freeze_command(): ++ expected = [sys.executable, "-m", "pip", "freeze"] ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pycrypto==2.6", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.freeze() ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ assert ret == eggs ++ ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ # Passing env_vars passes them to underlying command? ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.freeze(env_vars={"foo": "bar"}) ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ env={"foo": "bar"}, ++ ) ++ assert ret == eggs ++ ++ # Non zero returncode raises exception? ++ mock = MagicMock(return_value={"retcode": 1, "stderr": "CABOOOOMMM!"}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ pytest.raises( ++ CommandExecutionError, ++ pip.freeze, ++ ) ++ ++ ++def test_freeze_command_with_all(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pip==0.9.1", ++ "pycrypto==2.6", ++ "setuptools==20.10.1", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="9.0.1")): ++ ret = pip.freeze() ++ expected = [sys.executable, "-m", "pip", "freeze", "--all"] ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ assert ret == eggs ++ ++ # Non zero returncode raises exception? ++ mock = MagicMock(return_value={"retcode": 1, "stderr": "CABOOOOMMM!"}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="9.0.1")): ++ pytest.raises( ++ CommandExecutionError, ++ pip.freeze, ++ ) ++ ++ ++def test_list_command(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pycrypto==2.6", ++ ] ++ mock_version = "6.1.1" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value=mock_version)): ++ ret = pip.list_() ++ expected = [sys.executable, "-m", "pip", "freeze"] ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ python_shell=False, ++ use_vt=False, ++ ) ++ assert ret == { ++ "SaltTesting-dev": "git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8", ++ "M2Crypto": "0.21.1", ++ "bbfreeze-loader": "1.1.0", ++ "bbfreeze": "1.1.0", ++ "pip": mock_version, ++ "pycrypto": "2.6", ++ } ++ ++ # Non zero returncode raises exception? ++ mock = MagicMock(return_value={"retcode": 1, "stderr": "CABOOOOMMM!"}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ pytest.raises( ++ CommandExecutionError, ++ pip.list_, ++ ) ++ ++ ++def test_list_command_with_all(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pip==9.0.1", ++ "pycrypto==2.6", ++ "setuptools==20.10.1", ++ ] ++ # N.B.: this is deliberately different from the "output" of pip freeze. ++ # This is to demonstrate that the version reported comes from freeze ++ # instead of from the pip.version function. ++ mock_version = "9.0.0" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value=mock_version)): ++ ret = pip.list_() ++ expected = [sys.executable, "-m", "pip", "freeze", "--all"] ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ python_shell=False, ++ use_vt=False, ++ ) ++ assert ret == { ++ "SaltTesting-dev": "git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8", ++ "M2Crypto": "0.21.1", ++ "bbfreeze-loader": "1.1.0", ++ "bbfreeze": "1.1.0", ++ "pip": "9.0.1", ++ "pycrypto": "2.6", ++ "setuptools": "20.10.1", ++ } ++ ++ # Non zero returncode raises exception? ++ mock = MagicMock(return_value={"retcode": 1, "stderr": "CABOOOOMMM!"}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ pytest.raises( ++ CommandExecutionError, ++ pip.list_, ++ ) ++ ++ ++def test_list_command_with_prefix(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pycrypto==2.6", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.list_(prefix="bb") ++ expected = [sys.executable, "-m", "pip", "freeze"] ++ mock.assert_called_with( ++ expected, ++ cwd=None, ++ runas=None, ++ python_shell=False, ++ use_vt=False, ++ ) ++ assert ret == {"bbfreeze-loader": "1.1.0", "bbfreeze": "1.1.0"} ++ ++ ++def test_list_upgrades_legacy(): ++ eggs = [ ++ "apache-libcloud (Current: 1.1.0 Latest: 2.2.1 [wheel])", ++ "appdirs (Current: 1.4.1 Latest: 1.4.3 [wheel])", ++ "awscli (Current: 1.11.63 Latest: 1.12.1 [sdist])", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.list_upgrades() ++ mock.assert_called_with( ++ [sys.executable, "-m", "pip", "list", "--outdated"], ++ cwd=None, ++ runas=None, ++ ) ++ assert ret == { ++ "apache-libcloud": "2.2.1 [wheel]", ++ "appdirs": "1.4.3 [wheel]", ++ "awscli": "1.12.1 [sdist]", ++ } ++ ++ ++def test_list_upgrades_gt9(): ++ eggs = """[{"latest_filetype": "wheel", "version": "1.1.0", "name": "apache-libcloud", "latest_version": "2.2.1"}, ++ {"latest_filetype": "wheel", "version": "1.4.1", "name": "appdirs", "latest_version": "1.4.3"}, ++ {"latest_filetype": "sdist", "version": "1.11.63", "name": "awscli", "latest_version": "1.12.1"} ++ ]""" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "{}".format(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="9.1.1")): ++ ret = pip.list_upgrades() ++ mock.assert_called_with( ++ [ ++ sys.executable, ++ "-m", ++ "pip", ++ "list", ++ "--outdated", ++ "--format=json", ++ ], ++ cwd=None, ++ runas=None, ++ ) ++ assert ret == { ++ "apache-libcloud": "2.2.1 [wheel]", ++ "appdirs": "1.4.3 [wheel]", ++ "awscli": "1.12.1 [sdist]", ++ } ++ ++ ++def test_is_installed_true(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pycrypto==2.6", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.is_installed(pkgname="bbfreeze") ++ mock.assert_called_with( ++ [sys.executable, "-m", "pip", "freeze"], ++ cwd=None, ++ runas=None, ++ python_shell=False, ++ use_vt=False, ++ ) ++ assert ret ++ ++ ++def test_is_installed_false(): ++ eggs = [ ++ "M2Crypto==0.21.1", ++ "-e git+git@github.com:s0undt3ch/salt-testing.git@9ed81aa2f918d59d3706e56b18f0782d1ea43bf8#egg=SaltTesting-dev", ++ "bbfreeze==1.1.0", ++ "bbfreeze-loader==1.1.0", ++ "pycrypto==2.6", ++ ] ++ mock = MagicMock(return_value={"retcode": 0, "stdout": "\n".join(eggs)}) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="6.1.1")): ++ ret = pip.is_installed(pkgname="notexist") ++ mock.assert_called_with( ++ [sys.executable, "-m", "pip", "freeze"], ++ cwd=None, ++ runas=None, ++ python_shell=False, ++ use_vt=False, ++ ) ++ assert not ret ++ ++ ++def test_install_pre_argument_in_resulting_command(): ++ pkg = "pep8" ++ # Lower than 1.4 versions don't end up with `--pre` in the resulting output ++ mock = MagicMock( ++ side_effect=[ ++ {"retcode": 0, "stdout": "pip 1.2.0 /path/to/site-packages/pip"}, ++ {"retcode": 0, "stdout": ""}, ++ ] ++ ) ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}): ++ with patch("salt.modules.pip.version", MagicMock(return_value="1.3")): ++ pip.install(pkg, pre_releases=True) ++ expected = [sys.executable, "-m", "pip", "install", pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ mock_run = MagicMock(return_value="pip 1.4.1 /path/to/site-packages/pip") ++ mock_run_all = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ with patch.dict( ++ pip.__salt__, {"cmd.run_stdout": mock_run, "cmd.run_all": mock_run_all} ++ ): ++ with patch("salt.modules.pip._get_pip_bin", MagicMock(return_value=["pip"])): ++ pip.install(pkg, pre_releases=True) ++ expected = ["pip", "install", "--pre", pkg] ++ mock_run_all.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ ++ ++def test_resolve_requirements_chain_function(): ++ with patch("salt.utils.files.fopen", FakeFopen): ++ chain = pip._resolve_requirements_chain( ++ ["requirements-0.txt", "requirements-3.txt"] ++ ) ++ assert chain == [ ++ "requirements-0.txt", ++ "requirements-1.txt", ++ "requirements-2.txt", ++ "requirements-3.txt", ++ "requirements-4.txt", ++ ] ++ ++ ++def test_when_upgrade_is_called_and_there_are_available_upgrades_it_should_call_correct_command( ++ expected_user, ++): ++ fake_run_all = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ pip_user = expected_user ++ with patch.dict(pip.__salt__, {"cmd.run_all": fake_run_all}), patch( ++ "salt.modules.pip.list_upgrades", autospec=True, return_value=[pip_user] ++ ), patch( ++ "salt.modules.pip._get_pip_bin", ++ autospec=True, ++ return_value=["some-other-pip"], ++ ): ++ pip.upgrade(user=pip_user) ++ ++ fake_run_all.assert_any_call( ++ ["some-other-pip", "install", "-U", "freeze", "--all", pip_user], ++ runas=pip_user, ++ cwd=None, ++ use_vt=False, ++ ) ++ ++ ++def test_when_list_upgrades_is_provided_a_user_it_should_be_passed_to_the_version_command( ++ expected_user, ++): ++ fake_run_all = MagicMock(return_value={"retcode": 0, "stdout": "{}"}) ++ pip_user = expected_user ++ ++ def all_new_commands(*args, **kwargs): ++ """ ++ Without this, mutating the return value mutates the return value ++ for EVERYTHING. ++ """ ++ return ["some-other-pip"] ++ ++ with patch.dict(pip.__salt__, {"cmd.run_all": fake_run_all}), patch( ++ "salt.modules.pip._get_pip_bin", ++ autospec=True, ++ side_effect=all_new_commands, ++ ): ++ pip._clear_context() ++ pip.list_upgrades(user=pip_user) ++ fake_run_all.assert_any_call( ++ ["some-other-pip", "--version"], ++ runas=expected_user, ++ cwd=None, ++ python_shell=False, ++ ) ++ ++ ++def test_when_install_is_provided_a_user_it_should_be_passed_to_the_version_command( ++ expected_user, ++): ++ fake_run_all = MagicMock(return_value={"retcode": 0, "stdout": "{}"}) ++ pip_user = expected_user ++ ++ def all_new_commands(*args, **kwargs): ++ """ ++ Without this, mutating the return value mutates the return value ++ for EVERYTHING. ++ """ ++ return ["some-other-pip"] ++ ++ with patch.dict(pip.__salt__, {"cmd.run_all": fake_run_all}), patch( ++ "salt.modules.pip._get_pip_bin", ++ autospec=True, ++ side_effect=all_new_commands, ++ ): ++ pip._clear_context() ++ pip.install(user=pip_user) ++ fake_run_all.assert_any_call( ++ ["some-other-pip", "--version"], ++ runas=pip_user, ++ cwd=None, ++ python_shell=False, ++ ) ++ ++ ++def test_when_version_is_called_with_a_user_it_should_be_passed_to_undelying_runas( ++ expected_user, ++): ++ fake_run_all = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ pip_user = expected_user ++ with patch.dict(pip.__salt__, {"cmd.run_all": fake_run_all}), patch( ++ "salt.modules.pip.list_upgrades", autospec=True, return_value=[pip_user] ++ ), patch( ++ "salt.modules.pip._get_pip_bin", ++ autospec=True, ++ return_value=["some-new-pip"], ++ ): ++ pip.version(user=pip_user) ++ fake_run_all.assert_called_with( ++ ["some-new-pip", "--version"], ++ runas=pip_user, ++ cwd=None, ++ python_shell=False, ++ ) ++ ++ ++def test_install_target_from_VENV_PIP_TARGET_in_resulting_command(): ++ pkg = "pep8" ++ target = "/tmp/foo" ++ target_env = "/tmp/bar" ++ mock = MagicMock(return_value={"retcode": 0, "stdout": ""}) ++ environment = os.environ.copy() ++ environment["VENV_PIP_TARGET"] = target_env ++ with patch.dict(pip.__salt__, {"cmd.run_all": mock}), patch.object( ++ os, "environ", environment ++ ): ++ pip.install(pkg) ++ expected = [sys.executable, "-m", "pip", "install", "--target", target_env, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) ++ mock.reset_mock() ++ pip.install(pkg, target=target) ++ expected = [sys.executable, "-m", "pip", "install", "--target", target, pkg] ++ mock.assert_called_with( ++ expected, ++ saltenv="base", ++ runas=None, ++ use_vt=False, ++ python_shell=False, ++ ) +-- +2.36.1 + + diff --git a/use-salt-bundle-in-dockermod.patch b/use-salt-bundle-in-dockermod.patch new file mode 100644 index 0000000..ad73b16 --- /dev/null +++ b/use-salt-bundle-in-dockermod.patch @@ -0,0 +1,375 @@ +From ed53e3cbd62352b8d2af4d4b36c03e40981263bb Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Mon, 27 Jun 2022 17:59:24 +0300 +Subject: [PATCH] Use Salt Bundle in dockermod + +* Use Salt Bundle for salt calls in dockermod + +* Add test of performing a call with the Salt Bundle +--- + salt/modules/dockermod.py | 197 +++++++++++++++--- + .../unit/modules/dockermod/test_module.py | 78 ++++++- + 2 files changed, 241 insertions(+), 34 deletions(-) + +diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py +index fdded88dbb..e6b81e7f09 100644 +--- a/salt/modules/dockermod.py ++++ b/salt/modules/dockermod.py +@@ -201,14 +201,19 @@ import copy + import fnmatch + import functools + import gzip ++import hashlib + import json + import logging + import os ++import pathlib + import pipes + import re + import shutil + import string + import subprocess ++import sys ++import tarfile ++import tempfile + import time + import uuid + +@@ -6682,6 +6687,111 @@ def _compile_state(sls_opts, mods=None): + return st_.state.compile_high_data(high_data) + + ++def gen_venv_tar(cachedir, venv_dest_dir, venv_name): ++ """ ++ Generate tarball with the Salt Bundle if required and return the path to it ++ """ ++ exec_path = pathlib.Path(sys.executable).parts ++ venv_dir_name = "venv-salt-minion" ++ if venv_dir_name not in exec_path: ++ return None ++ ++ venv_tar = os.path.join(cachedir, "venv-salt.tgz") ++ venv_hash = os.path.join(cachedir, "venv-salt.hash") ++ venv_lock = os.path.join(cachedir, ".venv-salt.lock") ++ ++ venv_path = os.path.join(*exec_path[0 : exec_path.index(venv_dir_name)]) ++ ++ with __utils__["files.flopen"](venv_lock, "w"): ++ start_dir = os.getcwd() ++ venv_hash_file = os.path.join(venv_path, venv_dir_name, "venv-hash.txt") ++ try: ++ with __utils__["files.fopen"](venv_hash_file, "r") as fh: ++ venv_hash_src = fh.readline().strip() ++ except Exception: # pylint: disable=broad-except ++ # It makes no sense what caused the exception ++ # Just calculate the hash different way ++ for cmd in ("rpm -qi venv-salt-minion", "dpkg -s venv-salt-minion"): ++ ret = __salt__["cmd.run_all"]( ++ cmd, ++ python_shell=True, ++ clean_env=True, ++ env={"LANG": "C", "LANGUAGE": "C", "LC_ALL": "C"}, ++ ) ++ if ret.get("retcode") == 0 and ret.get("stdout"): ++ venv_hash_src = hashlib.sha256( ++ "{}\n".format(ret.get("stdout")).encode() ++ ).hexdigest() ++ break ++ try: ++ with __utils__["files.fopen"](venv_hash, "r") as fh: ++ venv_hash_dest = fh.readline().strip() ++ except Exception: # pylint: disable=broad-except ++ # It makes no sense what caused the exception ++ # Set the hash to impossible value to force new tarball creation ++ venv_hash_dest = "UNKNOWN" ++ if venv_hash_src == venv_hash_dest and os.path.isfile(venv_tar): ++ return venv_tar ++ try: ++ tfd, tmp_venv_tar = tempfile.mkstemp( ++ dir=cachedir, ++ prefix=".venv-", ++ suffix=os.path.splitext(venv_tar)[1], ++ ) ++ os.close(tfd) ++ ++ os.chdir(venv_path) ++ tfp = tarfile.open(tmp_venv_tar, "w:gz") ++ ++ for root, dirs, files in salt.utils.path.os_walk( ++ venv_dir_name, followlinks=True ++ ): ++ for name in files: ++ if name == "python" and pathlib.Path(root).parts == ( ++ venv_dir_name, ++ "bin", ++ ): ++ tfd, tmp_python_file = tempfile.mkstemp( ++ dir=cachedir, ++ prefix=".python-", ++ ) ++ os.close(tfd) ++ try: ++ with __utils__["files.fopen"]( ++ os.path.join(root, name), "r" ++ ) as fh_in: ++ with __utils__["files.fopen"]( ++ tmp_python_file, "w" ++ ) as fh_out: ++ rd_lines = fh_in.readlines() ++ rd_lines = [ ++ 'export VIRTUAL_ENV="{}"\n'.format( ++ os.path.join(venv_dest_dir, venv_name) ++ ) ++ if line.startswith("export VIRTUAL_ENV=") ++ else line ++ for line in rd_lines ++ ] ++ fh_out.write("".join(rd_lines)) ++ os.chmod(tmp_python_file, 0o755) ++ tfp.add(tmp_python_file, arcname=os.path.join(root, name)) ++ continue ++ finally: ++ if os.path.isfile(tmp_python_file): ++ os.remove(tmp_python_file) ++ if not name.endswith((".pyc", ".pyo")): ++ tfp.add(os.path.join(root, name)) ++ ++ tfp.close() ++ shutil.move(tmp_venv_tar, venv_tar) ++ with __utils__["files.fopen"](venv_hash, "w") as fh: ++ fh.write("{}\n".format(venv_hash_src)) ++ finally: ++ os.chdir(start_dir) ++ ++ return venv_tar ++ ++ + def call(name, function, *args, **kwargs): + """ + Executes a Salt function inside a running container +@@ -6717,47 +6827,68 @@ def call(name, function, *args, **kwargs): + if function is None: + raise CommandExecutionError("Missing function parameter") + +- # move salt into the container +- thin_path = __utils__["thin.gen_thin"]( +- __opts__["cachedir"], +- extra_mods=__salt__["config.option"]("thin_extra_mods", ""), +- so_mods=__salt__["config.option"]("thin_so_mods", ""), +- ) +- ret = copy_to( +- name, thin_path, os.path.join(thin_dest_path, os.path.basename(thin_path)) +- ) ++ venv_dest_path = "/var/tmp" ++ venv_name = "venv-salt-minion" ++ venv_tar = gen_venv_tar(__opts__["cachedir"], venv_dest_path, venv_name) + +- # figure out available python interpreter inside the container (only Python3) +- pycmds = ("python3", "/usr/libexec/platform-python") +- container_python_bin = None +- for py_cmd in pycmds: +- cmd = [py_cmd] + ["--version"] +- ret = run_all(name, subprocess.list2cmdline(cmd)) +- if ret["retcode"] == 0: +- container_python_bin = py_cmd +- break +- if not container_python_bin: +- raise CommandExecutionError( +- "Python interpreter cannot be found inside the container. Make sure Python is installed in the container" ++ if venv_tar is not None: ++ venv_python_bin = os.path.join(venv_dest_path, venv_name, "bin", "python") ++ dest_venv_tar = os.path.join(venv_dest_path, os.path.basename(venv_tar)) ++ copy_to(name, venv_tar, dest_venv_tar, overwrite=True, makedirs=True) ++ run_all( ++ name, ++ subprocess.list2cmdline( ++ ["tar", "zxf", dest_venv_tar, "-C", venv_dest_path] ++ ), ++ ) ++ run_all(name, subprocess.list2cmdline(["rm", "-f", dest_venv_tar])) ++ container_python_bin = venv_python_bin ++ thin_dest_path = os.path.join(venv_dest_path, venv_name) ++ thin_salt_call = os.path.join(thin_dest_path, "bin", "salt-call") ++ else: ++ # move salt into the container ++ thin_path = __utils__["thin.gen_thin"]( ++ __opts__["cachedir"], ++ extra_mods=__salt__["config.option"]("thin_extra_mods", ""), ++ so_mods=__salt__["config.option"]("thin_so_mods", ""), + ) + +- # untar archive +- untar_cmd = [ +- container_python_bin, +- "-c", +- 'import tarfile; tarfile.open("{0}/{1}").extractall(path="{0}")'.format( +- thin_dest_path, os.path.basename(thin_path) +- ), +- ] +- ret = run_all(name, subprocess.list2cmdline(untar_cmd)) +- if ret["retcode"] != 0: +- return {"result": False, "comment": ret["stderr"]} ++ ret = copy_to( ++ name, thin_path, os.path.join(thin_dest_path, os.path.basename(thin_path)) ++ ) ++ ++ # figure out available python interpreter inside the container (only Python3) ++ pycmds = ("python3", "/usr/libexec/platform-python") ++ container_python_bin = None ++ for py_cmd in pycmds: ++ cmd = [py_cmd] + ["--version"] ++ ret = run_all(name, subprocess.list2cmdline(cmd)) ++ if ret["retcode"] == 0: ++ container_python_bin = py_cmd ++ break ++ if not container_python_bin: ++ raise CommandExecutionError( ++ "Python interpreter cannot be found inside the container. Make sure Python is installed in the container" ++ ) ++ ++ # untar archive ++ untar_cmd = [ ++ container_python_bin, ++ "-c", ++ 'import tarfile; tarfile.open("{0}/{1}").extractall(path="{0}")'.format( ++ thin_dest_path, os.path.basename(thin_path) ++ ), ++ ] ++ ret = run_all(name, subprocess.list2cmdline(untar_cmd)) ++ if ret["retcode"] != 0: ++ return {"result": False, "comment": ret["stderr"]} ++ thin_salt_call = os.path.join(thin_dest_path, "salt-call") + + try: + salt_argv = ( + [ + container_python_bin, +- os.path.join(thin_dest_path, "salt-call"), ++ thin_salt_call, + "--metadata", + "--local", + "--log-file", +diff --git a/tests/pytests/unit/modules/dockermod/test_module.py b/tests/pytests/unit/modules/dockermod/test_module.py +index 47fe5d55e6..19c7f450d7 100644 +--- a/tests/pytests/unit/modules/dockermod/test_module.py ++++ b/tests/pytests/unit/modules/dockermod/test_module.py +@@ -3,6 +3,7 @@ Unit tests for the docker module + """ + + import logging ++import sys + + import pytest + import salt.config +@@ -26,6 +27,7 @@ def configure_loader_modules(): + whitelist=[ + "args", + "docker", ++ "files", + "json", + "state", + "thin", +@@ -880,13 +882,16 @@ def test_call_success(): + client = Mock() + client.put_archive = Mock() + get_client_mock = MagicMock(return_value=client) ++ gen_venv_tar_mock = MagicMock(return_value=None) + + context = {"docker.exec_driver": "docker-exec"} + salt_dunder = {"config.option": docker_config_mock} + + with patch.object(docker_mod, "run_all", docker_run_all_mock), patch.object( + docker_mod, "copy_to", docker_copy_to_mock +- ), patch.object(docker_mod, "_get_client", get_client_mock), patch.dict( ++ ), patch.object(docker_mod, "_get_client", get_client_mock), patch.object( ++ docker_mod, "gen_venv_tar", gen_venv_tar_mock ++ ), patch.dict( + docker_mod.__opts__, {"cachedir": "/tmp"} + ), patch.dict( + docker_mod.__salt__, salt_dunder +@@ -931,6 +936,11 @@ def test_call_success(): + != docker_run_all_mock.mock_calls[9][1][1] + ) + ++ # check the parameters of gen_venv_tar call ++ assert gen_venv_tar_mock.mock_calls[0][1][0] == "/tmp" ++ assert gen_venv_tar_mock.mock_calls[0][1][1] == "/var/tmp" ++ assert gen_venv_tar_mock.mock_calls[0][1][2] == "venv-salt-minion" ++ + assert {"retcode": 0, "comment": "container cmd"} == ret + + +@@ -1352,3 +1362,69 @@ def test_port(): + "bar": {"6666/tcp": ports["bar"]["6666/tcp"]}, + "baz": {}, + } ++ ++ ++@pytest.mark.slow_test ++def test_call_with_gen_venv_tar(): ++ """ ++ test module calling inside containers with the Salt Bundle ++ """ ++ ret = None ++ docker_run_all_mock = MagicMock( ++ return_value={ ++ "retcode": 0, ++ "stdout": '{"retcode": 0, "comment": "container cmd"}', ++ "stderr": "err", ++ } ++ ) ++ docker_copy_to_mock = MagicMock(return_value={"retcode": 0}) ++ docker_config_mock = MagicMock(return_value="") ++ docker_cmd_run_mock = MagicMock( ++ return_value={ ++ "retcode": 0, ++ "stdout": "test", ++ } ++ ) ++ client = Mock() ++ client.put_archive = Mock() ++ get_client_mock = MagicMock(return_value=client) ++ ++ context = {"docker.exec_driver": "docker-exec"} ++ salt_dunder = { ++ "config.option": docker_config_mock, ++ "cmd.run_all": docker_cmd_run_mock, ++ } ++ ++ with patch.object(docker_mod, "run_all", docker_run_all_mock), patch.object( ++ docker_mod, "copy_to", docker_copy_to_mock ++ ), patch.object(docker_mod, "_get_client", get_client_mock), patch.object( ++ sys, "executable", "/tmp/venv-salt-minion/bin/python" ++ ), patch.dict( ++ docker_mod.__opts__, {"cachedir": "/tmp"} ++ ), patch.dict( ++ docker_mod.__salt__, salt_dunder ++ ), patch.dict( ++ docker_mod.__context__, context ++ ): ++ ret = docker_mod.call("ID", "test.arg", 1, 2, arg1="val1") ++ ++ # Check that the directory is different each time ++ # [ call(name, [args]), ... ++ assert "mkdir" in docker_run_all_mock.mock_calls[0][1][1] ++ ++ assert ( ++ "tar zxf /var/tmp/venv-salt.tgz -C /var/tmp" ++ == docker_run_all_mock.mock_calls[1][1][1] ++ ) ++ ++ assert docker_run_all_mock.mock_calls[3][1][1].startswith( ++ "/var/tmp/venv-salt-minion/bin/python /var/tmp/venv-salt-minion/bin/salt-call " ++ ) ++ ++ # check remove the salt bundle tarball ++ assert docker_run_all_mock.mock_calls[2][1][1] == "rm -f /var/tmp/venv-salt.tgz" ++ ++ # check directory cleanup ++ assert docker_run_all_mock.mock_calls[4][1][1] == "rm -rf /var/tmp/venv-salt-minion" ++ ++ assert {"retcode": 0, "comment": "container cmd"} == ret +-- +2.36.1 + +