diff --git a/changelog/67975.fixed.md b/changelog/67975.fixed.md new file mode 100644 index 000000000000..8f386a340fed --- /dev/null +++ b/changelog/67975.fixed.md @@ -0,0 +1 @@ +Improve support for installing via groups with dnf5 on Fedora 41/42 diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py index bfdf1da84071..4ea47c426dcd 100644 --- a/salt/modules/yumpkg.py +++ b/salt/modules/yumpkg.py @@ -2512,6 +2512,28 @@ def group_list(): "available language groups:": "available languages", } + if _yum() == "dnf5": + out = __salt__["cmd.run_stdout"]( + [_yum(), "group", "list", "--hidden"], + output_loglevel="trace", + python_shell=False, + ) + + for line in salt.utils.itertools.split(out, "\n"): + line_lc = line.lower() + # split line into 3 parts: ID (no spaces), Name (contains spaces), and + # Installed (one of 'yes' or 'no') + match = re.match(r"^(\S+?)\s+(.+?)\s*(yes|no)$", line_lc) + if match: + pkg_id, pkg_name, pkg_installed = match.groups() + if pkg_id != "id": + if pkg_installed == "yes": + ret["installed"].append(pkg_id) + else: + ret["available"].append(pkg_id) + return ret + + # else: not dnf5 out = __salt__["cmd.run_stdout"]( [_yum(), "grouplist", "hidden"], output_loglevel="trace", python_shell=False ) @@ -2615,7 +2637,10 @@ def group_info(name, expand=False, ignore_groups=None, **kwargs): } ) - cmd = [_yum(), "--quiet"] + options + ["groupinfo", name] + if _yum() == "dnf5": + cmd = [_yum(), "--quiet"] + options + ["group", "info", name] + else: + cmd = [_yum(), "--quiet"] + options + ["groupinfo", name] out = __salt__["cmd.run_stdout"](cmd, output_loglevel="trace", python_shell=False) g_info = {} @@ -2630,9 +2655,15 @@ def group_info(name, expand=False, ignore_groups=None, **kwargs): ret["type"] = "environment group" elif "group" in g_info: ret["type"] = "package group" + elif "name" in g_info: + ret["type"] = "package group" - ret["group"] = g_info.get("environment group") or g_info.get("group") - ret["id"] = g_info.get("environment-id") or g_info.get("group-id") + ret["group"] = ( + g_info.get("environment group") or g_info.get("group") or g_info.get("name") + ) + ret["id"] = ( + g_info.get("environment-id") or g_info.get("group-id") or g_info.get("id") + ) if not ret["group"] and not ret["id"]: raise CommandExecutionError(f"Group '{name}' not found") @@ -2643,7 +2674,8 @@ def group_info(name, expand=False, ignore_groups=None, **kwargs): for pkgtype in pkgtypes: target_found = False for line in salt.utils.itertools.split(out, "\n"): - line = line.strip().lstrip(string.punctuation) + line = line.strip().lstrip(string.punctuation).lstrip() + # dnf match = re.match( pkgtypes_capturegroup + r" (?:groups|packages):\s*$", line.lower() ) @@ -2656,6 +2688,29 @@ def group_info(name, expand=False, ignore_groups=None, **kwargs): # We've reached the targeted section target_found = True continue + # dnf5 + match_dnf5 = re.match( + pkgtypes_capturegroup + r" (?:groups|packages)\s*:\s*(.*?)$", + line.lower(), + ) + if match_dnf5: + if target_found: + # We've reached a new section, break from loop + break + else: + if match_dnf5.group(1) == pkgtype: + # We've reached the targeted section + target_found = True + # The difference here from dnf (above) is that this line + # also contains the first package of this section. + # Have to pull out the package name, but not changing the case + rematch_dnf5 = re.match(r"^.*:\s*(.*?)$", line) + # Let line be the match we found, and then simply + # continue on, where we'll add this to the appropriate group + # Can't fail... + if rematch_dnf5: + line = rematch_dnf5.group(1) + if target_found: if expand and ret["type"] == "environment group": if not line or line in completed_groups: diff --git a/tests/pytests/unit/modules/test_yumpkg.py b/tests/pytests/unit/modules/test_yumpkg.py index cfc2af7e5bcc..8b08353481de 100644 --- a/tests/pytests/unit/modules/test_yumpkg.py +++ b/tests/pytests/unit/modules/test_yumpkg.py @@ -3256,3 +3256,80 @@ def test_normalize_name_with_arch_x86_64_v2(): assert yumpkg.normalize_name("chrony.x86_64") == "chrony.x86_64" with patch("salt.utils.pkg.rpm.get_osarch", MagicMock(return_value="x86_64")): assert yumpkg.normalize_name("rootfiles.noarch") == "rootfiles" + + +def test_67975_dnf5_group_info(): + """ + Test yumpkg.group_info parsing for dnf5 format + """ + patch_yum = patch("salt.modules.yumpkg._yum", Mock(return_value="dnf5")) + expected = { + "mandatory": [ + "libreoffice-calc", + "libreoffice-emailmerge", + "libreoffice-graphicfilter", + "libreoffice-impress", + "libreoffice-writer", + ], + "optional": [ + "libreoffice-base", + "libreoffice-draw", + "libreoffice-math", + "libreoffice-pyuno", + ], + "default": [], + "conditional": [], + "type": "package group", + "group": "LibreOffice", + "id": "libreoffice", + "description": "LibreOffice Productivity Suite", + } + cmd_out = """Id : libreoffice + Name : LibreOffice + Description : LibreOffice Productivity Suite + Installed : yes + Order : + Langonly : + Uservisible : yes + Repositories : @System + Mandatory packages : libreoffice-calc + : libreoffice-emailmerge + : libreoffice-graphicfilter + : libreoffice-impress + : libreoffice-writer + Optional packages : libreoffice-base + : libreoffice-draw + : libreoffice-math + : libreoffice-pyuno""" + with patch_yum: + with patch.dict( + yumpkg.__salt__, {"cmd.run_stdout": MagicMock(return_value=cmd_out)} + ): + info = yumpkg.group_info("libreoffice") + assert info == expected + + +def test_67975_dnf5_group_list(): + patch_yum = patch("salt.modules.yumpkg._yum", Mock(return_value="dnf5")) + mock_out = MagicMock( + return_value="""\ +ID Name Installed +foo Foo package no +bar Bar package no +brackets Just (testing) yes yes +cleaners Mop and bucket yes +last But not least no\ + """ + ) + patch_grplist = patch.dict(yumpkg.__salt__, {"cmd.run_stdout": mock_out}) + with patch_yum: + with patch_grplist: + result = yumpkg.group_list() + expected = { + "installed": ["brackets", "cleaners"], + "available": ["foo", "bar"], + "installed environments": [], + "available environments": [], + "available languages": {}, + } + assert result == expected