From 8110a9447631398f011aaed1bd0c7c3697d6ea01 Mon Sep 17 00:00:00 2001
From: Michael Calmer <Michael.Calmer@suse.de>
Date: Mon, 23 Jan 2023 14:33:17 +0100
Subject: [PATCH] 3004 implement zypper removeptf (#574)

* 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 <mwilhite@vmware.com>

Co-authored-by: Megan Wilhite <mwilhite@vmware.com>
---
 changelog/63442.added                        |  1 +
 salt/modules/zypperpkg.py                    | 38 +++++++-
 tests/pytests/unit/modules/test_zypperpkg.py | 92 +++++++++++++++++++-
 tests/unit/modules/test_zypperpkg.py         |  1 +
 4 files changed, 130 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 9eee162086..cd057ec637 100644
--- a/salt/modules/zypperpkg.py
+++ b/salt/modules/zypperpkg.py
@@ -2060,17 +2060,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()
@@ -2082,6 +2086,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)
@@ -2170,6 +2181,11 @@ def remove(
         salt '*' pkg.remove <package name>
         salt '*' pkg.remove <package1>,<package2>,<package3>
         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)
 
@@ -2728,6 +2744,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 e02bba9a07..316936f74f 100644
--- a/tests/pytests/unit/modules/test_zypperpkg.py
+++ b/tests/pytests/unit/modules/test_zypperpkg.py
@@ -10,7 +10,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
@@ -528,3 +528,93 @@ def test_dist_upgrade_failure():
 
             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 bcd001cd85..db766363ec 100644
--- a/tests/unit/modules/test_zypperpkg.py
+++ b/tests/unit/modules/test_zypperpkg.py
@@ -1470,6 +1470,7 @@ class ZypperTestCase(TestCase, LoaderModuleMockMixin):
         # 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.37.3


