From 3eebb26e00fd584ac030cfed6f3f8c02726636e0 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Thu, 11 Jun 2026 17:24:03 -0700 Subject: [PATCH] Fix pkg.group_installed false failure on unavailable group members yum/dnf treat "No match for group package " as a warning and exit 0 when a group declares a package that no enabled repository provides (commonly arch-specific subpackages like pcp-pmda-kvm). The pkg.group_installed state, however, marked the run as failed for every group target missing from pkg.list_pkgs() after install, including those default/optional members the package manager itself skipped. Restrict the post-install missing-package check to mandatory group members and the user-supplied include list, matching the package manager's own success criteria. Fixes #68210 --- changelog/68210.fixed.md | 1 + salt/states/pkg.py | 15 +++++- tests/pytests/unit/states/test_pkg.py | 70 +++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 changelog/68210.fixed.md diff --git a/changelog/68210.fixed.md b/changelog/68210.fixed.md new file mode 100644 index 000000000000..61ee3baf1cb8 --- /dev/null +++ b/changelog/68210.fixed.md @@ -0,0 +1 @@ +Fixed ``pkg.group_installed`` reporting failure on RPM-based systems when a package group's default or optional members are not available in any enabled repository. The state now only considers mandatory group members and explicitly requested ``include`` packages when checking for install failures, matching the behavior of ``yum/dnf group install`` (which reports "No match for group package" but still exits 0). diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 494fdca7b57b..194274153906 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -3437,7 +3437,8 @@ def group_installed(name, skip=None, include=None, **kwargs): ) return ret - targets = diff["mandatory"]["not installed"] + mandatory_targets = list(diff["mandatory"]["not installed"]) + targets = list(mandatory_targets) targets.extend([x for x in diff["default"]["not installed"] if x not in skip]) targets.extend(include) @@ -3478,7 +3479,17 @@ def group_installed(name, skip=None, include=None, **kwargs): ) return ret - failed = [x for x in targets if x not in __salt__["pkg.list_pkgs"](**kwargs)] + # Only flag a failure when a *mandatory* group member is missing after + # install, or when an explicitly user-requested ``include`` package is + # missing. Default/optional group members that the package manager could + # not install (e.g. arch-specific subpackages not present in any enabled + # repo) match the underlying ``yum/dnf group install`` behavior, which + # reports "No match for group package " and still exits 0. Treating + # those as state failures contradicts the package manager's own result + # and surfaces as a spurious red state run -- see #68210. + required = list(mandatory_targets) + list(include) + installed_pkgs = __salt__["pkg.list_pkgs"](**kwargs) + failed = [x for x in required if x not in installed_pkgs] if failed: ret["comment"] = "Failed to install the following packages: {}".format( ", ".join(failed) diff --git a/tests/pytests/unit/states/test_pkg.py b/tests/pytests/unit/states/test_pkg.py index f9c566524df7..27af024df15f 100644 --- a/tests/pytests/unit/states/test_pkg.py +++ b/tests/pytests/unit/states/test_pkg.py @@ -1143,6 +1143,76 @@ def test_pacmanpkg_group_installed_with_repo_options(list_pkgs): assert ret["comment"] == "Repo options are not supported on this platform" +def test_group_installed_unavailable_optional_member_68210(): + """ + Regression test for #68210. + + pkg.group_installed must not fail when a group's default/optional member + does not exist in any enabled repository. dnf/yum itself reports such + "No match for group package" cases as success, and they should not + flip the state's result to False. + """ + name = "Performance Tools" + # The group declares pcp-pmda-kvm as a default member, but the repo + # does not provide it on this arch. yum/dnf installs the rest and + # exits 0; pkg.install returns the actually-installed pkgs only. + diff = { + "mandatory": {"installed": [], "not installed": []}, + "default": { + "installed": [], + "not installed": ["perf", "pcp-pmda-kvm"], + }, + "optional": {"installed": [], "not installed": []}, + "conditional": {"installed": [], "not installed": []}, + } + group_diff_mock = MagicMock(return_value=diff) + install_mock = MagicMock(return_value={"perf": {"old": "", "new": "6.12.0"}}) + list_pkgs_after = MagicMock(return_value={"perf": "6.12.0"}) + + salt_dict = { + "pkg.group_diff": group_diff_mock, + "pkg.install": install_mock, + "pkg.list_pkgs": list_pkgs_after, + } + + with patch.dict(pkg.__salt__, salt_dict): + ret = pkg.group_installed(name) + + assert ret["result"] is True, ret + assert ret["changes"] == {"perf": {"old": "", "new": "6.12.0"}} + assert "Failed to install" not in ret["comment"] + + +def test_group_installed_mandatory_member_missing_still_fails_68210(): + """ + Companion to test_group_installed_unavailable_optional_member_68210: + if a *mandatory* group member fails to install, the state must still + fail. Only default/optional members are forgiven when missing. + """ + name = "Critical Group" + diff = { + "mandatory": {"installed": [], "not installed": ["required-pkg"]}, + "default": {"installed": [], "not installed": []}, + "optional": {"installed": [], "not installed": []}, + "conditional": {"installed": [], "not installed": []}, + } + group_diff_mock = MagicMock(return_value=diff) + install_mock = MagicMock(return_value={}) + list_pkgs_after = MagicMock(return_value={}) + + salt_dict = { + "pkg.group_diff": group_diff_mock, + "pkg.install": install_mock, + "pkg.list_pkgs": list_pkgs_after, + } + + with patch.dict(pkg.__salt__, salt_dict): + ret = pkg.group_installed(name) + + assert ret["result"] is False, ret + assert "required-pkg" in ret["comment"] + + def test_latest(): """ Test pkg.latest