diff --git a/changelog/67171.fixed.md b/changelog/67171.fixed.md new file mode 100644 index 00000000000..d0d51a507ac --- /dev/null +++ b/changelog/67171.fixed.md @@ -0,0 +1,3 @@ +Fix a stacktrace on Windows with pkg.installed and test=True. The +`pkg.list_repo_pkgs` function does not exist on Windows. This uses the +`pkg.list_available` function instead for Windows. diff --git a/salt/modules/win_pkg.py b/salt/modules/win_pkg.py index 8b834bbd33e..52ed0b92c88 100644 --- a/salt/modules/win_pkg.py +++ b/salt/modules/win_pkg.py @@ -45,6 +45,7 @@ import re import sys import time import urllib.parse +from fnmatch import fnmatch from functools import cmp_to_key import salt.fileserver @@ -275,6 +276,11 @@ def list_available(*names, **kwargs): return_dict_always (bool): Default ``False`` dict when a single package name is queried. + reverse_sort (bool): + Sort the versions for latest to oldest + + .. versionadded:: 3007.2 + Returns: dict: The package name with its available versions @@ -298,12 +304,15 @@ def list_available(*names, **kwargs): return_dict_always = salt.utils.data.is_true( kwargs.get("return_dict_always", False) ) + reverse_sort = salt.utils.data.is_true(kwargs.get("reverse_sort", False)) if len(names) == 1 and not return_dict_always: pkginfo = _get_package_info(names[0], saltenv=saltenv) if not pkginfo: return "" versions = sorted( - list(pkginfo.keys()), key=cmp_to_key(_reverse_cmp_pkg_versions) + list(pkginfo.keys()), + key=cmp_to_key(_reverse_cmp_pkg_versions), + reverse=reverse_sort, ) else: versions = {} @@ -314,11 +323,70 @@ def list_available(*names, **kwargs): verlist = sorted( list(pkginfo.keys()) if pkginfo else [], key=cmp_to_key(_reverse_cmp_pkg_versions), + reverse=reverse_sort, ) versions[name] = verlist return versions +def list_repo_pkgs(*args, saltenv="base", **kwargs): + """ + .. versionadded:: 3007.2 + + This function was added to match a similar function in Linux. It will + return all available packages. Optionally, package names (and name globs) + can be passed and the results will be filtered to packages matching those + names. + + This function can be helpful in discovering the version or repo to specify + in a :mod:`pkg.installed ` state. + + The return data will be a dictionary mapping package names to a list of + version numbers, ordered from newest to oldest. For example: + + .. code-block:: python + + { + 'bash': ['4.3-14ubuntu1.1', + '4.3-14ubuntu1'], + 'nginx': ['1.10.0-0ubuntu0.16.04.4', + '1.9.15-0ubuntu1'] + } + + CLI Examples: + + .. code-block:: bash + + salt '*' pkg.list_repo_pkgs + salt '*' pkg.list_repo_pkgs foo bar baz + """ + + # Get all the repo data + pkgs = get_repo_data(saltenv=saltenv).get("repo", {}) + + # Generate a list of packages and their available versions + repo_pkgs = {} + for pkg in pkgs: + repo_pkgs.update({pkg: list(pkgs[pkg].keys())}) + + # If no args passed, just return everything + if not args: + return repo_pkgs + + # Loop through the args and return info for each specified package + ret = {} + for arg in args: + for pkg in repo_pkgs: + if fnmatch(pkg, arg): + ret.update({pkg: repo_pkgs[pkg]}) + + # If ret is empty, return everything + if not ret: + ret = repo_pkgs + + return ret + + def version(*names, **kwargs): """ Returns a string representing the package version or an empty string if not diff --git a/tests/pytests/functional/modules/test_win_pkg.py b/tests/pytests/functional/modules/test_win_pkg.py index 6bcfaa9bd84..8742a1c2c9c 100644 --- a/tests/pytests/functional/modules/test_win_pkg.py +++ b/tests/pytests/functional/modules/test_win_pkg.py @@ -13,9 +13,26 @@ def pkg_def_contents(state_tree): my-software: '1.0.1': full_name: 'My Software' - installer: 'C:\files\mysoftware.msi' + installer: 'C:\files\mysoftware101.msi' install_flags: '/qn /norestart' - uninstaller: 'C:\files\mysoftware.msi' + uninstaller: 'C:\files\mysoftware101.msi' + uninstall_flags: '/qn /norestart' + msiexec: True + reboot: False + '1.0.2': + full_name: 'My Software' + installer: 'C:\files\mysoftware102.msi' + install_flags: '/qn /norestart' + uninstaller: 'C:\files\mysoftware102.msi' + uninstall_flags: '/qn /norestart' + msiexec: True + reboot: False + your-software: + '1.0.0': + full_name: 'Your Software' + installer: 'C:\files\yoursoftware101.msi' + install_flags: '/qn /norestart' + uninstaller: 'C:\files\yoursoftware101.msi' uninstall_flags: '/qn /norestart' msiexec: True reboot: False @@ -27,9 +44,47 @@ def pkg(modules): yield modules.pkg -def test_refresh_db(pkg, pkg_def_contents, state_tree, minion_opts): +@pytest.fixture(scope="function") +def repo(pkg, state_tree, pkg_def_contents): assert len(pkg.get_package_info("my-software")) == 0 repo_dir = state_tree / "winrepo_ng" with pytest.helpers.temp_file("my-software.sls", pkg_def_contents, repo_dir): pkg.refresh_db() - assert len(pkg.get_package_info("my-software")) == 1 + + +def test_refresh_db(pkg, repo): + assert len(pkg.get_package_info("my-software")) == 2 + assert len(pkg.get_package_info("your-software")) == 1 + + +@pytest.mark.parametrize( + "as_dict, reverse, expected", + [ + (False, False, ["1.0.1", "1.0.2"]), + (False, True, ["1.0.2", "1.0.1"]), + (True, False, {"my-software": ["1.0.1", "1.0.2"]}), + (True, True, {"my-software": ["1.0.2", "1.0.1"]}), + ], +) +def test_list_available(pkg, repo, as_dict, reverse, expected): + result = pkg.list_available( + "my-software", return_dict_always=as_dict, reverse_sort=reverse + ) + assert result == expected + + +@pytest.mark.parametrize( + "pkg_name, expected", + [ + ("my-software", {"my-software": ["1.0.1", "1.0.2"]}), + ("my-soft*", {"my-software": ["1.0.1", "1.0.2"]}), + ("your-software", {"your-software": ["1.0.0"]}), + (None, {"my-software": ["1.0.1", "1.0.2"], "your-software": ["1.0.0"]}), + ], +) +def test_list_repo_pkgs(pkg, repo, pkg_name, expected): + if pkg_name is None: + result = pkg.list_repo_pkgs() + else: + result = pkg.list_repo_pkgs(pkg_name) + assert result == expected diff --git a/tests/pytests/functional/states/test_pkg.py b/tests/pytests/functional/states/test_pkg.py index d179eaa4ca0..31f85ef1133 100644 --- a/tests/pytests/functional/states/test_pkg.py +++ b/tests/pytests/functional/states/test_pkg.py @@ -58,7 +58,7 @@ def refresh_keys(grains, modules): def PKG_TARGETS(grains): _PKG_TARGETS = ["figlet", "sl"] if grains["os"] == "Windows": - _PKG_TARGETS = ["npp_x64", "winrar"] + _PKG_TARGETS = ["npp_x64", "putty"] elif grains["os"] == "Amazon": if grains["osfinger"] == "Amazon Linux-2023": _PKG_TARGETS = ["lynx", "gnuplot-minimal"] @@ -225,6 +225,29 @@ def install_7zip(modules): assert "22.01.00.0" not in versions +@pytest.fixture(scope="module") +def pkg_def_contents(state_tree): + return r""" + my-software: + '1.0.1': + full_name: 'My Software' + installer: 'C:\files\mysoftware101.msi' + install_flags: '/qn /norestart' + uninstaller: 'C:\files\mysoftware101.msi' + uninstall_flags: '/qn /norestart' + msiexec: True + reboot: False + '1.0.2': + full_name: 'My Software' + installer: 'C:\files\mysoftware102.msi' + install_flags: '/qn /norestart' + uninstaller: 'C:\files\mysoftware102.msi' + uninstall_flags: '/qn /norestart' + msiexec: True + reboot: False + """ + + @pytest.mark.requires_salt_modules("pkg.version") @pytest.mark.requires_salt_states("pkg.installed", "pkg.removed") @pytest.mark.slow_test @@ -1126,3 +1149,14 @@ def test_pkg_removed_with_version_multiple(install_7zip, modules, states): assert ret.result is True current = modules.pkg.version("7zip") assert "22.01.00.0" in current + + +@pytest.mark.skip_unless_on_windows() +def test_pkg_latest_test_true(states, modules, state_tree, pkg_def_contents): + repo_dir = state_tree / "winrepo_ng" + with pytest.helpers.temp_file("my-software.sls", pkg_def_contents, repo_dir): + modules.pkg.refresh_db() + assert len(modules.pkg.get_package_info("my-software")) == 2 + result = states.pkg.latest("my-software", test=True) + expected = {"my-software": {"new": "1.0.2", "old": ""}} + assert result.changes == expected