From 327a5e5b24c4fa047df44b245abd672e02999cca Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Mon, 23 Jan 2023 14:33:26 +0100 Subject: [PATCH] 3005.1 implement zypper removeptf (#573) * handle ptf packages inside of normal pkg.remove function * add testcase for remove and removeptf * add changelog * adapt old tests to changed function * Update Docs Co-authored-by: Megan Wilhite --- changelog/63442.added | 1 + salt/modules/zypperpkg.py | 38 +- tests/pytests/unit/modules/test_zypperpkg.py | 356 ++++++++++++++++++- tests/unit/modules/test_zypperpkg.py | 1 + 4 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 changelog/63442.added diff --git a/changelog/63442.added b/changelog/63442.added new file mode 100644 index 0000000000..ad81b2f9d5 --- /dev/null +++ b/changelog/63442.added @@ -0,0 +1 @@ +implement removal of ptf packages in zypper pkg module diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py index 051f8f72c7..44f2cdbd3a 100644 --- a/salt/modules/zypperpkg.py +++ b/salt/modules/zypperpkg.py @@ -2073,17 +2073,21 @@ def _uninstall(inclusion_detection, name=None, pkgs=None, root=None): except MinionError as exc: raise CommandExecutionError(exc) + ptfpackages = _find_ptf_packages(pkg_params.keys(), root=root) includes = _detect_includes(pkg_params.keys(), inclusion_detection) old = list_pkgs(root=root, includes=includes) targets = [] for target in pkg_params: + if target in ptfpackages: + # ptfpackages needs special handling + continue # Check if package version set to be removed is actually installed: # old[target] contains a comma-separated list of installed versions if target in old and pkg_params[target] in old[target].split(","): targets.append(target + "-" + pkg_params[target]) elif target in old and not pkg_params[target]: targets.append(target) - if not targets: + if not targets and not ptfpackages: return {} systemd_scope = _systemd_scope() @@ -2095,6 +2099,13 @@ def _uninstall(inclusion_detection, name=None, pkgs=None, root=None): ) targets = targets[500:] + # handle ptf packages + while ptfpackages: + __zypper__(systemd_scope=systemd_scope, root=root).call( + "removeptf", "--allow-downgrade", *ptfpackages[:500] + ) + ptfpackages = ptfpackages[500:] + _clean_cache() new = list_pkgs(root=root, includes=includes) ret = salt.utils.data.compare_dicts(old, new) @@ -2183,6 +2194,11 @@ def remove( salt '*' pkg.remove salt '*' pkg.remove ,, salt '*' pkg.remove pkgs='["foo", "bar"]' + + .. versionchanged:: 3007 + Can now remove also PTF packages which require a different handling in the backend. + + Can now remove also PTF packages which require a different handling in the backend. """ return _uninstall(inclusion_detection, name=name, pkgs=pkgs, root=root) @@ -2658,6 +2674,26 @@ def _get_visible_patterns(root=None): return patterns +def _find_ptf_packages(pkgs, root=None): + """ + Find ptf packages in "pkgs" and return them as list + """ + ptfs = [] + cmd = ["rpm"] + if root: + cmd.extend(["--root", root]) + cmd.extend(["-q", "--qf", "%{NAME}: [%{PROVIDES} ]\n"]) + cmd.extend(pkgs) + output = __salt__["cmd.run"](cmd) + for line in output.splitlines(): + if not line.strip(): + continue + pkg, provides = line.split(":", 1) + if "ptf()" in provides: + ptfs.append(pkg) + return ptfs + + def _get_installed_patterns(root=None): """ List all installed patterns. diff --git a/tests/pytests/unit/modules/test_zypperpkg.py b/tests/pytests/unit/modules/test_zypperpkg.py index 91132b7277..c996662e1c 100644 --- a/tests/pytests/unit/modules/test_zypperpkg.py +++ b/tests/pytests/unit/modules/test_zypperpkg.py @@ -11,7 +11,7 @@ import pytest import salt.modules.pkg_resource as pkg_resource import salt.modules.zypperpkg as zypper from salt.exceptions import CommandExecutionError, SaltInvocationError -from tests.support.mock import MagicMock, mock_open, patch +from tests.support.mock import MagicMock, mock_open, call, patch @pytest.fixture @@ -27,6 +27,11 @@ def configure_loader_modules(): } +@pytest.fixture(autouse=True) +def fresh_zypper_instance(): + zypper.__zypper__ = zypper._Zypper() + + def test_list_pkgs_no_context(): """ Test packages listing. @@ -395,3 +400,352 @@ def test_del_repo_key(): with patch.dict(zypper.__salt__, salt_mock): assert zypper.del_repo_key(keyid="keyid", root="/mnt") salt_mock["lowpkg.remove_gpg_key"].assert_called_once_with("keyid", "/mnt") + +@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 + + +def test_remove_multiple_pkgs_with_ptf(): + call_spy = MagicMock() + zypper_mock = MagicMock() + zypper_mock.stdout = "" + zypper_mock.stderr = "" + zypper_mock.exit_code = 0 + zypper_mock.call = call_spy + + rpm_output = textwrap.dedent( + """ + vim: vi vim vim(x86-64) vim-base vim-enhanced vim-python vim_client + ptf-12345: ptf() ptf-12345 + """ + ) + rpm_mock = MagicMock(side_effect=[rpm_output]) + + with patch( + "salt.modules.zypperpkg.__zypper__", MagicMock(return_value=zypper_mock) + ), patch.object( + zypper, + "list_pkgs", + MagicMock(side_effect=[{"vim": "0.18.0", "ptf-12345": "1"}, {}]), + ), patch.dict( + zypper.__salt__, {"cmd.run": rpm_mock} + ): + expected_calls = [ + call( + "remove", + "vim", + ), + call( + "removeptf", + "--allow-downgrade", + "ptf-12345", + ), + ] + + result = zypper.remove(name="vim,ptf-12345") + call_spy.assert_has_calls(expected_calls, any_order=False) + assert result["vim"]["new"] == "", result + assert result["vim"]["old"] == "0.18.0", result + assert result["ptf-12345"]["new"] == "", result + assert result["ptf-12345"]["old"] == "1", result + + +def test_remove_ptf(): + call_spy = MagicMock() + zypper_mock = MagicMock() + zypper_mock.stdout = "" + zypper_mock.stderr = "" + zypper_mock.exit_code = 0 + zypper_mock.call = call_spy + + rpm_mock = MagicMock( + side_effect=[ + "vim: vi vim vim(x86-64) vim-base vim-enhanced vim-python vim_client", + "ptf-12345: ptf() ptf-12345", + ] + ) + + with patch( + "salt.modules.zypperpkg.__zypper__", MagicMock(return_value=zypper_mock) + ), patch.object( + zypper, + "list_pkgs", + MagicMock(side_effect=[{"vim": "0.18.0"}, {}, {"ptf-12345": "1"}, {}]), + ), patch.dict( + zypper.__salt__, {"cmd.run": rpm_mock} + ): + expected_call_vim = [ + "remove", + "vim", + ] + expected_call_ptf = [ + "removeptf", + "--allow-downgrade", + "ptf-12345", + ] + + result = zypper.remove(name="vim") + call_spy.assert_called_with(*expected_call_vim) + assert result["vim"]["new"] == "", result + assert result["vim"]["old"] == "0.18.0", result + + result = zypper.remove(name="ptf-12345") + call_spy.assert_called_with(*expected_call_ptf) + assert result["ptf-12345"]["new"] == "", result + assert result["ptf-12345"]["old"] == "1", result diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py index f5b6d74b6f..6e5ca88895 100644 --- a/tests/unit/modules/test_zypperpkg.py +++ b/tests/unit/modules/test_zypperpkg.py @@ -1953,6 +1953,7 @@ Repository 'DUMMY' not found by its alias, number, or URI. # If config.get starts being used elsewhere, we'll need to write a # side_effect function. patches = { + "cmd.run": MagicMock(return_value="vim: vi vim\npico: pico"), "cmd.run_all": MagicMock(return_value=cmd_out), "pkg_resource.parse_targets": MagicMock(return_value=parsed_targets), "pkg_resource.stringify": MagicMock(), -- 2.39.2