salt/tests/pytests/unit/states/test_pkg.py
bdrx312 66caa58346 Fix issues with requisites and aggregate
add install of networkx
fix aggregate to properly work with requisites
fix requisite checking to not be exponential
fix pkg aggregate to work when multiple states specify the same package
add some type hints to state.py to make the code easier to follow
fix case of pkg aggregate duplicate package.
2024-10-10 01:53:52 -07:00

1294 lines
40 KiB
Python

import logging
import textwrap
import pytest
import salt.modules.beacons as beaconmod
import salt.modules.cp as cp
import salt.modules.pacmanpkg as pacmanpkg
import salt.modules.pkg_resource as pkg_resource
import salt.modules.yumpkg as yumpkg
import salt.states.beacon as beaconstate
import salt.states.pkg as pkg
import salt.utils.state as state_utils
from salt.utils.event import SaltEvent
from tests.support.mock import MagicMock, patch
log = logging.getLogger(__name__)
@pytest.fixture
def configure_loader_modules(minion_opts):
return {
cp: {
"__opts__": minion_opts,
},
pkg: {
"__env__": "base",
"__salt__": {},
"__grains__": {"os": "CentOS", "os_family": "RedHat"},
"__opts__": minion_opts,
"__instance_id__": "",
"__low__": {},
"__utils__": {"state.gen_tag": state_utils.gen_tag},
},
beaconstate: {
"__salt__": {},
"__opts__": minion_opts,
},
beaconmod: {
"__salt__": {},
"__opts__": minion_opts,
},
pacmanpkg: {
"__salt__": {},
"__opts__": minion_opts,
},
pkg_resource: {
"__salt__": {},
"__grains__": {"os": "CentOS", "os_family": "RedHat"},
},
yumpkg: {
"__salt__": {},
"__grains__": {"osarch": "x86_64", "osmajorrelease": 7},
"__opts__": minion_opts,
},
}
@pytest.fixture(scope="module")
def pkgs():
return {
"pkga": {"old": "1.0.1", "new": "2.0.1"},
"pkgb": {"old": "1.0.2", "new": "2.0.2"},
"pkgc": {"old": "1.0.3", "new": "2.0.3"},
}
@pytest.fixture(scope="module")
def list_pkgs():
return {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
}
def test_uptodate_with_changes(pkgs):
"""
Test pkg.uptodate with simulated changes
"""
list_upgrades = MagicMock(
return_value={pkgname: pkgver["new"] for pkgname, pkgver in pkgs.items()}
)
upgrade = MagicMock(return_value=pkgs)
version = MagicMock(side_effect=lambda pkgname, **_: pkgs[pkgname]["old"])
with patch.dict(
pkg.__salt__,
{
"pkg.list_upgrades": list_upgrades,
"pkg.upgrade": upgrade,
"pkg.version": version,
},
):
# Run state with test=false
with patch.dict(pkg.__opts__, {"test": False}):
ret = pkg.uptodate("dummy")
assert ret["result"]
assert ret["changes"] == pkgs
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.uptodate("dummy")
assert ret["result"] is None
assert ret["changes"] == pkgs
def test_uptodate_with_pkgs_with_changes(pkgs):
"""
Test pkg.uptodate with simulated changes
"""
list_upgrades = MagicMock(
return_value={pkgname: pkgver["new"] for pkgname, pkgver in pkgs.items()}
)
upgrade = MagicMock(return_value=pkgs)
version = MagicMock(side_effect=lambda pkgname, **_: pkgs[pkgname]["old"])
with patch.dict(
pkg.__salt__,
{
"pkg.list_upgrades": list_upgrades,
"pkg.upgrade": upgrade,
"pkg.version": version,
},
):
# Run state with test=false
with patch.dict(pkg.__opts__, {"test": False}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert ret["result"]
assert ret["changes"] == pkgs
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert ret["result"] is None
assert ret["changes"] == pkgs
def test_uptodate_no_changes():
"""
Test pkg.uptodate with no changes
"""
list_upgrades = MagicMock(return_value={})
upgrade = MagicMock(return_value={})
with patch.dict(
pkg.__salt__, {"pkg.list_upgrades": list_upgrades, "pkg.upgrade": upgrade}
):
# Run state with test=false
with patch.dict(pkg.__opts__, {"test": False}):
ret = pkg.uptodate("dummy")
assert ret["result"]
assert ret["changes"] == {}
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.uptodate("dummy")
assert ret["result"]
assert ret["changes"] == {}
def test_uptodate_with_pkgs_no_changes(pkgs):
"""
Test pkg.uptodate with no changes
"""
list_upgrades = MagicMock(return_value={})
upgrade = MagicMock(return_value={})
with patch.dict(
pkg.__salt__, {"pkg.list_upgrades": list_upgrades, "pkg.upgrade": upgrade}
):
# Run state with test=false
with patch.dict(pkg.__opts__, {"test": False}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert ret["result"]
assert ret["changes"] == {}
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert ret["result"]
assert ret["changes"] == {}
def test_uptodate_with_failed_changes(pkgs):
"""
Test pkg.uptodate with simulated failed changes
"""
list_upgrades = MagicMock(
return_value={pkgname: pkgver["new"] for pkgname, pkgver in pkgs.items()}
)
upgrade = MagicMock(return_value={})
version = MagicMock(side_effect=lambda pkgname, **_: pkgs[pkgname]["old"])
with patch.dict(
pkg.__salt__,
{
"pkg.list_upgrades": list_upgrades,
"pkg.upgrade": upgrade,
"pkg.version": version,
},
):
# Run state with test=false
with patch.dict(pkg.__opts__, {"test": False}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert not ret["result"]
assert ret["changes"] == {}
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.uptodate(
"dummy",
test=True,
pkgs=[pkgname for pkgname in pkgs],
)
assert ret["result"] is None
assert ret["changes"] == pkgs
@pytest.mark.parametrize(
"version_string, expected_version_conditions",
[
(
"> 1.0.0, < 15.0.0, != 14.0.1",
[(">", "1.0.0"), ("<", "15.0.0"), ("!=", "14.0.1")],
),
(
"> 1.0.0,< 15.0.0,!= 14.0.1",
[(">", "1.0.0"), ("<", "15.0.0"), ("!=", "14.0.1")],
),
(">= 1.0.0, < 15.0.0", [(">=", "1.0.0"), ("<", "15.0.0")]),
(">=1.0.0,< 15.0.0", [(">=", "1.0.0"), ("<", "15.0.0")]),
("< 15.0.0", [("<", "15.0.0")]),
("<15.0.0", [("<", "15.0.0")]),
("15.0.0", [("==", "15.0.0")]),
("", []),
],
)
def test_parse_version_string(version_string, expected_version_conditions):
version_conditions = pkg._parse_version_string(version_string)
assert len(expected_version_conditions) == len(version_conditions)
for expected_version_condition, version_condition in zip(
expected_version_conditions, version_conditions
):
assert expected_version_condition[0] == version_condition[0]
assert expected_version_condition[1] == version_condition[1]
@pytest.mark.parametrize(
"version_string, installed_versions, expected_result",
[
("> 1.0.0, < 15.0.0, != 14.0.1", [], False),
("> 1.0.0, < 15.0.0, != 14.0.1", ["1.0.0"], False),
("> 1.0.0, < 15.0.0, != 14.0.1", ["14.0.1"], False),
("> 1.0.0, < 15.0.0, != 14.0.1", ["16.0.0"], False),
("> 1.0.0, < 15.0.0, != 14.0.1", ["2.0.0"], True),
(
"> 1.0.0, < 15.0.0, != 14.0.1",
["1.0.0", "14.0.1", "16.0.0", "2.0.0"],
True,
),
("> 15.0.0", [], False),
("> 15.0.0", ["1.0.0"], False),
("> 15.0.0", ["16.0.0"], True),
("15.0.0", [], False),
("15.0.0", ["15.0.0"], True),
# No version specified, whatever version installed. This is threated like ANY version installed fulfills.
("", ["15.0.0"], True),
# No version specified, no version installed.
("", [], False),
],
)
def test_fulfills_version_string(version_string, installed_versions, expected_result):
msg = "version_string: {}, installed_versions: {}, expected_result: {}".format(
version_string, installed_versions, expected_result
)
assert expected_result == pkg._fulfills_version_string(
installed_versions, version_string
)
@pytest.mark.parametrize(
"installed_versions, operator, version, expected_result",
[
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], "==", "1.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], ">=", "1.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], ">", "1.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], "<", "2.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], "<=", "2.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], "!=", "1.0.0", True),
(["1.0.0", "14.0.1", "16.0.0", "2.0.0"], "==", "17.0.0", False),
(["1.0.0"], "!=", "1.0.0", False),
([], "==", "17.0.0", False),
],
)
def test_fulfills_version_spec(installed_versions, operator, version, expected_result):
msg = (
"installed_versions: {}, operator: {}, version: {}, expected_result: {}".format(
installed_versions, operator, version, expected_result
)
)
assert expected_result == pkg._fulfills_version_spec(
installed_versions, operator, version
)
@pytest.mark.usefixtures("mocked_tcp_pub_client")
def test_mod_beacon(tmp_path):
"""
Test to create a beacon based on a pkg
"""
name = "vim"
with patch.dict(pkg.__salt__, {"beacons.list": MagicMock(return_value={})}):
with patch.dict(pkg.__states__, {"beacon.present": beaconstate.present}):
ret = pkg.mod_beacon(name, sfun="latest")
expected = {
"name": name,
"changes": {},
"result": False,
"comment": (
"pkg.latest does not work with the mod_beacon state function"
),
}
assert ret == expected
ret = pkg.mod_beacon(name, sfun="installed")
expected = {
"name": name,
"changes": {},
"result": True,
"comment": "Not adding beacon.",
}
assert ret == expected
event_returns = [
{
"complete": True,
"tag": "/salt/minion/minion_beacons_list_complete",
"beacons": {},
},
{
"complete": True,
"tag": "/salt/minion/minion_beacons_list_complete",
"beacons": {},
},
{
"complete": True,
"tag": "/salt/minion/minion_beacons_list_available_complete",
"beacons": ["pkg"],
},
{
"valid": True,
"tag": "/salt/minion/minion_beacon_validation_complete",
"vcomment": "Valid beacon configuration",
},
{
"complete": True,
"tag": "/salt/minion/minion_beacon_add_complete",
"beacons": {
"beacon_pkg_vim": [
{"pkgs": [name]},
{"interval": 60},
{"beacon_module": "pkg"},
]
},
},
]
mock = MagicMock(return_value=True)
beacon_state_mocks = {
"beacons.list": beaconmod.list_,
"beacons.add": beaconmod.add,
"beacons.list_available": beaconmod.list_available,
"event.fire": mock,
}
beacon_mod_mocks = {"event.fire": mock}
sock_dir = str(tmp_path / "test-socks")
with patch.dict(pkg.__states__, {"beacon.present": beaconstate.present}):
with patch.dict(beaconstate.__salt__, beacon_state_mocks):
with patch.dict(beaconmod.__salt__, beacon_mod_mocks):
with patch.dict(
beaconmod.__opts__, {"beacons": {}, "sock_dir": sock_dir}
):
with patch.object(
SaltEvent, "get_event", side_effect=event_returns
):
ret = pkg.mod_beacon(name, sfun="installed", beacon=True)
expected = {
"name": "beacon_pkg_vim",
"changes": {},
"result": True,
"comment": "Adding beacon_pkg_vim to beacons",
}
assert ret == expected
def test_mod_aggregate():
"""
Test to mod_aggregate function
"""
low = {
"state": "pkg",
"name": "other_pkgs",
"pkgs": ["byobu"],
"aggregate": True,
"fun": "installed",
}
chunks = [
{
"state": "file",
"name": "/tmp/install-vim",
"__sls__": "47628",
"__env__": "base",
"__id__": "/tmp/install-vim",
"order": 10000,
"fun": "managed",
},
{
"state": "file",
"name": "/tmp/install-tmux",
"__sls__": "47628",
"__env__": "base",
"__id__": "/tmp/install-tmux",
"order": 10001,
"fun": "managed",
},
{
"state": "pkg",
"name": "other_pkgs",
"__sls__": "47628",
"__env __": "base",
"__id__": "other_pkgs",
"pkgs": ["byobu"],
"aggregate": True,
"order": 10002,
"fun": "installed",
},
{
"state": "pkg",
"name": "bc",
"__sls__": "47628",
"__env__": "base",
"__id__": "bc",
"hold": True,
"order": 10003,
"fun": "installed",
},
{
"state": "pkg",
"name": "vim",
"__sls__": "47628",
"__env__": "base",
"__id__": "vim",
"require": ["/tmp/install-vim"],
"order": 10004,
"fun": "installed",
},
{
"state": "pkg",
"name": "tmux",
"__sls__": "47628",
"__env__": "base",
"__id__": "tmux",
"require": ["/tmp/install-tmux"],
"order": 10005,
"fun": "installed",
},
{
"state": "pkgrepo",
"name": "deb https://packages.cloud.google.com/apt cloud-sdk main",
"__sls__": "47628",
"__env__": "base",
"__id__": "google-cloud-repo",
"humanname": "Google Cloud SDK",
"file": "/etc/apt/sources.list.d/google-cloud-sdk.list",
"key_url": "https://packages.cloud.google.com/apt/doc/apt-key.gpg",
"order": 10006,
"fun": "managed",
},
{
"state": "pkg",
"name": "google-cloud-sdk",
"__sls__": "47628",
"__env__": "base",
"__id__": "google-cloud-sdk",
"require": ["google-cloud-repo"],
"order": 10007,
"fun": "installed",
},
]
running = {
"file_|-/tmp/install-vim_| -/tmp/install-vim_|-managed": {
"changes": {},
"comment": "File /tmp/install-vim exists with proper permissions. No changes made.",
"name": "/tmp/install-vim",
"result": True,
"__sls__": "47628",
"__run_num__": 0,
"start_time": "18:41:20.987275",
"duration": 5.833,
"__id__": "/tmp/install-vim",
},
"file_|-/tmp/install-tmux_|-/tmp/install-tmux_|-managed": {
"changes": {},
"comment": "File /tmp/install-tmux exists with proper permissions. No changes made.",
"name": "/tmp/install-tmux",
"result": True,
"__sls__": "47628",
"__run_num__": 1,
"start_time": "18:41:20.993258",
"duration": 1.263,
"__id__": "/tmp/install-tmux",
},
}
expected = {
"pkgs": ["byobu", "vim", "tmux", "google-cloud-sdk"],
"name": "other_pkgs",
"fun": "installed",
"aggregate": True,
"state": "pkg",
}
res = pkg.mod_aggregate(low, chunks, running)
assert res == expected
def test_installed_with_changes_test_true(list_pkgs):
"""
Test pkg.installed with simulated changes
"""
latest_pkgs = MagicMock(return_value="some version here")
list_pkgs = MagicMock(return_value=list_pkgs)
with patch.dict(
pkg.__salt__,
{
"pkg.latest_version": latest_pkgs,
"pkg.list_pkgs": list_pkgs,
},
):
expected = {"dummy": {"new": "some version here", "old": ""}}
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg.installed("dummy")
assert ret["result"] is None
assert ret["changes"] == expected
def test_installed_with_sources(list_pkgs, tmp_path):
"""
Test pkg.installed with passing `sources`
"""
list_pkgs = MagicMock(return_value=list_pkgs)
pkg_source = tmp_path / "pkga-package-0.3.0.deb"
with patch.dict(
pkg.__salt__,
{
"cp.cache_file": cp.cache_file,
"pkg.list_pkgs": list_pkgs,
"pkg_resource.pack_sources": pkg_resource.pack_sources,
"lowpkg.bin_pkg_info": MagicMock(),
},
), patch("salt.fileclient.get_file_client", return_value=MagicMock()):
try:
ret = pkg.installed("install-pkgd", sources=[{"pkga": str(pkg_source)}])
assert ret["result"] is False
except TypeError as exc:
if "got multiple values for keyword argument 'saltenv'" in str(exc):
pytest.fail(f"TypeError should have not been raised: {exc}")
raise exc from None
@pytest.mark.parametrize("action", ["removed", "purged"])
def test_removed_purged_with_changes_test_true(list_pkgs, action):
"""
Test pkg.removed with simulated changes
"""
list_pkgs = MagicMock(return_value=list_pkgs)
mock_parse_targets = MagicMock(return_value=[{"pkga": None}, "repository"])
with patch.dict(
pkg.__salt__,
{
"pkg.list_pkgs": list_pkgs,
"pkg_resource.parse_targets": mock_parse_targets,
"pkg_resource.version_clean": MagicMock(return_value=None),
},
):
expected = {"pkga": {"new": f"{action}", "old": ""}}
pkg_actions = {"removed": pkg.removed, "purged": pkg.purged}
# Run state with test=true
with patch.dict(pkg.__opts__, {"test": True}):
ret = pkg_actions[action]("pkga")
assert ret["result"] is None
assert ret["changes"] == expected
@pytest.mark.parametrize(
"package_manager",
[("Zypper"), ("YUM/DNF"), ("APT")],
)
def test_held_unheld(package_manager):
"""
Test pkg.held and pkg.unheld with Zypper, YUM/DNF and APT
"""
if package_manager == "Zypper":
list_holds_func = "pkg.list_locks"
list_holds_mock = MagicMock(
return_value={
"bar": {
"type": "package",
"match_type": "glob",
"case_sensitive": "on",
},
"minimal_base": {
"type": "pattern",
"match_type": "glob",
"case_sensitive": "on",
},
"baz": {
"type": "package",
"match_type": "glob",
"case_sensitive": "on",
},
}
)
elif package_manager == "YUM/DNF":
list_holds_func = "pkg.list_holds"
list_holds_mock = MagicMock(
return_value=[
"bar-0:1.2.3-1.1.*",
"baz-0:2.3.4-2.1.*",
]
)
elif package_manager == "APT":
list_holds_func = "pkg.get_selections"
list_holds_mock = MagicMock(
return_value={
"hold": [
"bar",
"baz",
]
}
)
def pkg_hold(name, pkgs=None, *_args, **__kwargs):
if name and pkgs is None:
pkgs = [name]
ret = {}
for pkg in pkgs:
ret.update(
{
pkg: {
"name": pkg,
"changes": {"new": "hold", "old": ""},
"result": True,
"comment": f"Package {pkg} is now being held.",
}
}
)
return ret
def pkg_unhold(name, pkgs=None, *_args, **__kwargs):
if name and pkgs is None:
pkgs = [name]
ret = {}
for pkg in pkgs:
ret.update(
{
pkg: {
"name": pkg,
"changes": {"new": "", "old": "hold"},
"result": True,
"comment": f"Package {pkg} is no longer held.",
}
}
)
return ret
hold_mock = MagicMock(side_effect=pkg_hold)
unhold_mock = MagicMock(side_effect=pkg_unhold)
# Testing with Zypper
with patch.dict(
pkg.__salt__,
{
list_holds_func: list_holds_mock,
"pkg.hold": hold_mock,
"pkg.unhold": unhold_mock,
},
):
# Holding one of two packages
ret = pkg.held("held-test", pkgs=["foo", "bar"])
assert "foo" in ret["changes"]
assert len(ret["changes"]) == 1
hold_mock.assert_called_once_with(name="held-test", pkgs=["foo"])
unhold_mock.assert_not_called()
hold_mock.reset_mock()
unhold_mock.reset_mock()
# Holding one of two packages and replacing all the rest held packages
ret = pkg.held("held-test", pkgs=["foo", "bar"], replace=True)
assert "foo" in ret["changes"]
assert "baz" in ret["changes"]
assert len(ret["changes"]) == 2
hold_mock.assert_called_once_with(name="held-test", pkgs=["foo"])
unhold_mock.assert_called_once_with(name="held-test", pkgs=["baz"])
hold_mock.reset_mock()
unhold_mock.reset_mock()
# Remove all holds
ret = pkg.held("held-test", pkgs=[], replace=True)
assert "bar" in ret["changes"]
assert "baz" in ret["changes"]
assert len(ret["changes"]) == 2
hold_mock.assert_not_called()
unhold_mock.assert_any_call(name="held-test", pkgs=["baz"])
unhold_mock.assert_any_call(name="held-test", pkgs=["bar"])
hold_mock.reset_mock()
unhold_mock.reset_mock()
# Unolding one of two packages
ret = pkg.unheld("held-test", pkgs=["foo", "bar"])
assert "bar" in ret["changes"]
assert len(ret["changes"]) == 1
unhold_mock.assert_called_once_with(name="held-test", pkgs=["bar"])
hold_mock.assert_not_called()
hold_mock.reset_mock()
unhold_mock.reset_mock()
# Remove all holds
ret = pkg.unheld("held-test", all=True)
assert "bar" in ret["changes"]
assert "baz" in ret["changes"]
assert len(ret["changes"]) == 2
hold_mock.assert_not_called()
unhold_mock.assert_any_call(name="held-test", pkgs=["baz"])
unhold_mock.assert_any_call(name="held-test", pkgs=["bar"])
def test_installed_with_single_normalize():
"""
Test pkg.installed with preventing multiple package name normalisation
"""
list_no_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
}
list_no_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
}
list_with_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": "20220214-2.1",
}
list_with_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": ["20220214-2.1"],
}
list_pkgs = MagicMock(
side_effect=[
# For the package with version specified
list_no_weird_installed_ver_list,
{},
list_no_weird_installed,
list_no_weird_installed_ver_list,
list_with_weird_installed,
list_with_weird_installed_ver_list,
# For the package with no version specified
list_no_weird_installed_ver_list,
{},
list_no_weird_installed,
list_no_weird_installed_ver_list,
list_with_weird_installed,
list_with_weird_installed_ver_list,
]
)
salt_dict = {
"pkg.install": yumpkg.install,
"pkg.list_pkgs": list_pkgs,
"pkg.normalize_name": yumpkg.normalize_name,
"pkg_resource.version_clean": pkg_resource.version_clean,
"pkg_resource.parse_targets": pkg_resource.parse_targets,
}
with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch(
"salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)
), patch(
"salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0})
) as call_yum_mock, patch.dict(
pkg.__salt__, salt_dict
), patch.dict(
pkg_resource.__salt__, salt_dict
), patch.dict(
yumpkg.__salt__, salt_dict
), patch.dict(
yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7}
), patch.object(
yumpkg, "list_holds", MagicMock()
):
expected = {
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": {
"old": "",
"new": "20220214-2.1",
}
}
ret = pkg.installed(
"test_install",
pkgs=[{"weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch": "20220214-2.1"}],
)
call_yum_mock.assert_called_once()
assert (
"weird-name-1.2.3-1234.5.6.test7tst.x86_64-20220214-2.1"
in call_yum_mock.mock_calls[0].args[0]
)
assert ret["result"]
assert ret["changes"] == expected
call_yum_mock.reset_mock()
ret = pkg.installed(
"test_install",
pkgs=["weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch"],
)
call_yum_mock.assert_called_once()
assert (
"weird-name-1.2.3-1234.5.6.test7tst.x86_64"
in call_yum_mock.mock_calls[0].args[0]
)
assert ret["result"]
assert ret["changes"] == expected
def test_removed_with_single_normalize():
"""
Test pkg.removed with preventing multiple package name normalisation
"""
list_no_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
}
list_no_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
}
list_with_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": "20220214-2.1",
}
list_with_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": ["20220214-2.1"],
}
list_pkgs = MagicMock(
side_effect=[
# For the package with version specified
list_with_weird_installed_ver_list,
list_with_weird_installed,
list_no_weird_installed,
list_no_weird_installed_ver_list,
# For the package with no version specified
list_with_weird_installed_ver_list,
list_with_weird_installed,
list_no_weird_installed,
list_no_weird_installed_ver_list,
]
)
salt_dict = {
"pkg.remove": yumpkg.remove,
"pkg.list_pkgs": list_pkgs,
"pkg.normalize_name": yumpkg.normalize_name,
"pkg_resource.parse_targets": pkg_resource.parse_targets,
"pkg_resource.version_clean": pkg_resource.version_clean,
}
with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch(
"salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)
), patch(
"salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0})
) as call_yum_mock, patch.dict(
pkg.__salt__, salt_dict
), patch.dict(
pkg_resource.__salt__, salt_dict
), patch.dict(
yumpkg.__salt__, salt_dict
):
expected = {
"weird-name-1.2.3-1234.5.6.test7tst.x86_64": {
"old": "20220214-2.1",
"new": "",
}
}
ret = pkg.removed(
"test_remove",
pkgs=[{"weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch": "20220214-2.1"}],
)
call_yum_mock.assert_called_once()
assert (
"weird-name-1.2.3-1234.5.6.test7tst.x86_64-20220214-2.1"
in call_yum_mock.mock_calls[0].args[0]
)
assert ret["result"]
assert ret["changes"] == expected
call_yum_mock.reset_mock()
ret = pkg.removed(
"test_remove",
pkgs=["weird-name-1.2.3-1234.5.6.test7tst.x86_64.noarch"],
)
call_yum_mock.assert_called_once()
assert (
"weird-name-1.2.3-1234.5.6.test7tst.x86_64"
in call_yum_mock.mock_calls[0].args[0]
)
assert ret["result"]
assert ret["changes"] == expected
def test_installed_with_single_normalize_32bit():
"""
Test pkg.installed of 32bit package with preventing multiple package name normalisation
"""
list_no_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
}
list_no_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
}
list_with_weird_installed = {
"pkga": "1.0.1",
"pkgb": "1.0.2",
"pkgc": "1.0.3",
"xz-devel.i686": "1.2.3",
}
list_with_weird_installed_ver_list = {
"pkga": ["1.0.1"],
"pkgb": ["1.0.2"],
"pkgc": ["1.0.3"],
"xz-devel.i686": ["1.2.3"],
}
list_pkgs = MagicMock(
side_effect=[
list_no_weird_installed_ver_list,
{},
list_no_weird_installed,
list_no_weird_installed_ver_list,
list_with_weird_installed,
list_with_weird_installed,
list_with_weird_installed_ver_list,
]
)
salt_dict = {
"pkg.install": yumpkg.install,
"pkg.list_pkgs": list_pkgs,
"pkg.normalize_name": yumpkg.normalize_name,
"pkg_resource.version_clean": pkg_resource.version_clean,
"pkg_resource.parse_targets": pkg_resource.parse_targets,
}
with patch("salt.modules.yumpkg.list_pkgs", list_pkgs), patch(
"salt.modules.yumpkg.version_cmp", MagicMock(return_value=0)
), patch(
"salt.modules.yumpkg._call_yum", MagicMock(return_value={"retcode": 0})
) as call_yum_mock, patch.dict(
pkg.__salt__, salt_dict
), patch.dict(
pkg_resource.__salt__, salt_dict
), patch.dict(
yumpkg.__salt__, salt_dict
), patch.dict(
yumpkg.__grains__, {"os": "CentOS", "osarch": "x86_64", "osmajorrelease": 7}
):
expected = {
"xz-devel.i686": {
"old": "",
"new": "1.2.3",
}
}
ret = pkg.installed(
"test_install",
pkgs=["xz-devel.i686"],
)
call_yum_mock.assert_called_once()
assert "xz-devel.i686" in call_yum_mock.mock_calls[0].args[0]
assert ret["result"]
assert ret["changes"] == expected
def test__get_installable_versions_no_version_found():
mock_latest_versions = MagicMock(return_value={})
mock_list_repo_pkgs = MagicMock(return_value={})
with patch.dict(
pkg.__salt__,
{
"pkg.latest_version": mock_latest_versions,
"pkg.list_pkgs": mock_list_repo_pkgs,
},
), patch.dict(pkg.__opts__, {"test": True}):
expected = {"dummy": {"new": "installed", "old": ""}}
ret = pkg._get_installable_versions({"dummy": None}, current=None)
assert ret == expected
def test__get_installable_versions_version_found():
mock_latest_versions = MagicMock(return_value={"dummy": "1.0.1"})
mock_list_repo_pkgs = MagicMock(return_value={})
with patch.dict(
pkg.__salt__,
{
"pkg.latest_version": mock_latest_versions,
"pkg.list_pkgs": mock_list_repo_pkgs,
},
), patch.dict(pkg.__opts__, {"test": True}):
expected = {"dummy": {"new": "1.0.1", "old": ""}}
ret = pkg._get_installable_versions({"dummy": None}, current=None)
assert ret == expected
def test_installed_salt_minion_windows():
mock_list_pkgs = MagicMock(
return_value={
"git": "1.34.1",
"salt-minion-py3": "3006.0",
"vim": "1.6",
}
)
mock_install = MagicMock(
return_value={
"salt-minion-py3": {"install status": "task started"},
}
)
mock_find_install_targets = MagicMock(
return_value=(
{"salt-minion-py3": "3006.1"},
{"salt-minion-py3": "3006.1"},
[],
{},
{},
[],
True,
)
)
salt_dict = {
"pkg.install": mock_install,
"pkg.list_pkgs": mock_list_pkgs,
"pkg_resource.check_extra_requirements": pkg_resource.check_extra_requirements,
"pkg_resource.version_clean": pkg_resource.version_clean,
}
with patch.dict(pkg.__salt__, salt_dict), patch.object(
pkg, "_find_install_targets", mock_find_install_targets
):
expected = {
"salt-minion-py3": {"install status": "task started"},
}
ret = pkg.installed(name="salt-minion-py3", version="3006.1")
assert ret["result"]
assert ret["changes"] == expected
@pytest.mark.parametrize(
"kwargs, expected_cli_options",
(
(
(
"fromrepo=foo,bar",
"someotherkwarg=test",
"disablerepo=ignored",
"enablerepo=otherignored",
"disableexcludes=this_argument_is_also_ignored",
),
("--disablerepo=*", "--enablerepo=foo,bar"),
),
(
("enablerepo=foo", "disablerepo=bar"),
("--disablerepo=bar", "--enablerepo=foo"),
),
(
("disablerepo=foo",),
("--disablerepo=foo",),
),
(
("enablerepo=bar",),
("--enablerepo=bar",),
),
),
)
def test_yumpkg_group_installed_with_repo_options(
list_pkgs, kwargs, expected_cli_options
):
"""
Test that running a pkg.group_installed with repo options on RPM-based
systems results in the correct yum/dnf groupinfo command being run by
pkg.group_info.
"""
kwargs = dict(item.split("=", 1) for item in kwargs)
run_stdout = MagicMock(
return_value=textwrap.dedent(
"""\
Group: MyGroup
Group-Id: my-group
Description: A test group
Mandatory Packages:
pkga
pkgb
"""
)
)
salt_dict = {
"cmd.run_stdout": run_stdout,
"pkg.group_diff": yumpkg.group_diff,
"pkg.group_info": yumpkg.group_info,
}
name = "MyGroup"
with patch.dict(pkg.__salt__, salt_dict), patch.dict(
yumpkg.__salt__, salt_dict
), patch.object(
yumpkg,
"list_pkgs",
MagicMock(return_value=list_pkgs),
):
ret = pkg.group_installed(name, **kwargs)
assert ret["result"]
assert not ret["changes"]
expected = [yumpkg._yum(), "--quiet"]
expected.extend(expected_cli_options)
expected.extend(("groupinfo", name))
run_stdout.assert_called_once_with(
expected,
output_loglevel="trace",
python_shell=False,
)
def test_pacmanpkg_group_installed_with_repo_options(list_pkgs):
"""
Test that running a pkg.group_installed with additional arguments on
platforms which use pacman does not result in a traceback, but is instead
cleanly handled and a useful comment included in the state return.
"""
salt_dict = {
"pkg.group_diff": pacmanpkg.group_diff,
}
with patch.dict(pkg.__salt__, salt_dict), patch.dict(pacmanpkg.__salt__, salt_dict):
ret = pkg.group_installed("foo", fromrepo="bar")
assert not ret["result"]
assert not ret["changes"]
assert ret["comment"] == "Repo options are not supported on this platform"
def test_latest():
"""
Test pkg.latest
"""
pkg_name = "fake_pkg"
old_version = "1.2.2"
new_version = "1.2.3"
latest_version_mock = MagicMock(return_value={pkg_name: new_version})
current_version_mock = MagicMock(return_value={pkg_name: old_version})
install_mock = MagicMock(
return_value={
pkg_name: {
"new": new_version,
"old": old_version,
},
}
)
salt_dict = {
"pkg.latest_version": latest_version_mock,
"pkg.version": current_version_mock,
"pkg.install": install_mock,
}
with patch.dict(pkg.__salt__, salt_dict):
ret = pkg.latest(pkg_name)
assert ret.get("result", False) is True
def test_latest_multiple_versions():
"""
This case arises most often when updating the kernel, where multiple versions are now installed.
See: https://github.com/saltstack/salt/issues/60931
"""
pkg_name = "fake_pkg"
old_version = "1.2.2"
new_version = "1.2.3"
latest_version_mock = MagicMock(return_value={pkg_name: new_version})
current_version_mock = MagicMock(return_value={pkg_name: old_version})
install_mock = MagicMock(
return_value={
pkg_name: {
"new": f"{old_version},{new_version}",
"old": old_version,
},
}
)
salt_dict = {
"pkg.latest_version": latest_version_mock,
"pkg.version": current_version_mock,
"pkg.install": install_mock,
}
with patch.dict(pkg.__salt__, salt_dict):
ret = pkg.latest(pkg_name)
assert ret.get("result", False) is True
def test_latest_no_change_windows():
"""
Test pkg.latest with no change to the package version for winrepo packages
See: https://github.com/saltstack/salt/issues/65165
"""
pkg_name = "fake_pkg"
version = "1.2.2"
latest_version_mock = MagicMock(return_value={pkg_name: version})
current_version_mock = MagicMock(return_value={pkg_name: version})
install_mock = MagicMock(return_value={pkg_name: {"install status": "success"}})
salt_dict = {
"pkg.latest_version": latest_version_mock,
"pkg.version": current_version_mock,
"pkg.install": install_mock,
}
with patch.dict(pkg.__salt__, salt_dict):
ret = pkg.latest(pkg_name)
assert ret.get("result", False) is True