mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Covers the three cases that matter: apt drops recommends, another manager keeps its defaults, and a concrete module is left untouched.
270 lines
10 KiB
Python
270 lines
10 KiB
Python
import unittest
|
|
import unittest.mock as mock
|
|
|
|
from ansible.errors import AnsibleActionFail
|
|
from ansible.plugins.loader import become_loader, shell_loader
|
|
|
|
from plugins.action.package_install import ActionModule
|
|
from utils.packages.calls import GENERIC_PACKAGE, ModuleCall, RetryPolicy
|
|
|
|
|
|
def _action(args) -> ActionModule:
|
|
action = ActionModule.__new__(ActionModule)
|
|
action._task = mock.Mock(args=args)
|
|
return action
|
|
|
|
|
|
class TestPackageIds(unittest.TestCase):
|
|
def test_single_id(self):
|
|
self.assertEqual(_action({"id": "git"})._package_ids(), ["git"])
|
|
|
|
def test_list_of_ids(self):
|
|
self.assertEqual(
|
|
_action({"id": ["git", "curl"]})._package_ids(), ["git", "curl"]
|
|
)
|
|
|
|
def test_whitespace_is_trimmed_and_blanks_dropped(self):
|
|
self.assertEqual(_action({"id": [" git ", "", " "]})._package_ids(), ["git"])
|
|
|
|
def test_missing_id_fails(self):
|
|
with self.assertRaises(AnsibleActionFail):
|
|
_action({})._package_ids()
|
|
|
|
def test_empty_list_fails(self):
|
|
with self.assertRaises(AnsibleActionFail):
|
|
_action({"id": []})._package_ids()
|
|
|
|
|
|
class TestState(unittest.TestCase):
|
|
def test_defaults_to_present(self):
|
|
self.assertEqual(_action({"id": "git"})._state(), "present")
|
|
|
|
def test_absent_is_accepted(self):
|
|
self.assertEqual(_action({"id": "git", "state": "absent"})._state(), "absent")
|
|
|
|
def test_unknown_state_fails(self):
|
|
with self.assertRaises(AnsibleActionFail):
|
|
_action({"id": "git", "state": "latest"})._state()
|
|
|
|
|
|
class TestOwningRole(unittest.TestCase):
|
|
def setUp(self):
|
|
self.registry = {
|
|
"nfs-ganesha": mock.Mock(role="svc-storage-nfs-server", shared=False),
|
|
"git": mock.Mock(role=None, shared=True),
|
|
}
|
|
|
|
def _spec_for(self, package_id, role):
|
|
return _action({"id": package_id})._spec_for(
|
|
self.registry, package_id, "debian", "Debian", role
|
|
)
|
|
|
|
def test_foreign_role_id_fails(self):
|
|
with self.assertRaises(AnsibleActionFail) as caught:
|
|
self._spec_for("nfs-ganesha", "desk-micro")
|
|
self.assertIn("svc-storage-nfs-server", str(caught.exception))
|
|
|
|
def test_unknown_id_fails(self):
|
|
with self.assertRaises(AnsibleActionFail):
|
|
self._spec_for("nope", "desk-micro")
|
|
|
|
def test_own_id_resolves(self):
|
|
with mock.patch(
|
|
"plugins.action.package_install.resolve", return_value="spec"
|
|
) as resolver:
|
|
spec = self._spec_for("nfs-ganesha", "svc-storage-nfs-server")
|
|
self.assertEqual(spec, "spec")
|
|
resolver.assert_called_once()
|
|
|
|
def test_shared_id_resolves_for_any_role(self):
|
|
with mock.patch("plugins.action.package_install.resolve", return_value="spec"):
|
|
spec = self._spec_for("git", "desk-micro")
|
|
self.assertEqual(spec, "spec")
|
|
|
|
def test_no_role_context_skips_the_check(self):
|
|
with mock.patch("plugins.action.package_install.resolve", return_value="spec"):
|
|
spec = self._spec_for("nfs-ganesha", None)
|
|
self.assertEqual(spec, "spec")
|
|
|
|
|
|
class TestModuleName(unittest.TestCase):
|
|
def _module_name(self, module, facts):
|
|
return _action({})._module_name(ModuleCall(module, {}), facts)
|
|
|
|
def test_generic_package_becomes_the_hosts_package_manager(self):
|
|
self.assertEqual(
|
|
self._module_name(GENERIC_PACKAGE, {"pkg_mgr": "pacman"}), "pacman"
|
|
)
|
|
|
|
def test_concrete_modules_pass_through(self):
|
|
self.assertEqual(
|
|
self._module_name("community.general.copr", {"pkg_mgr": "dnf"}),
|
|
"community.general.copr",
|
|
)
|
|
|
|
def test_missing_pkg_mgr_fails(self):
|
|
with self.assertRaises(AnsibleActionFail):
|
|
self._module_name(GENERIC_PACKAGE, {})
|
|
|
|
|
|
class TestManagerArguments(unittest.TestCase):
|
|
def _args(self, module, facts):
|
|
action = _action({})
|
|
action._connection = mock.Mock(become=None, transport="local")
|
|
action._execute_module = mock.Mock(return_value={})
|
|
action._execute(ModuleCall(module, {"name": ["wireguard-tools"]}), {}, facts)
|
|
return action._execute_module.call_args.kwargs["module_args"]
|
|
|
|
def test_apt_installs_without_recommends(self):
|
|
args = self._args(GENERIC_PACKAGE, {"pkg_mgr": "apt"})
|
|
self.assertFalse(args["install_recommends"])
|
|
|
|
def test_other_managers_keep_their_defaults(self):
|
|
args = self._args(GENERIC_PACKAGE, {"pkg_mgr": "dnf"})
|
|
self.assertNotIn("install_recommends", args)
|
|
|
|
def test_a_concrete_module_is_left_alone(self):
|
|
args = self._args("ansible.builtin.apt", {"pkg_mgr": "apt"})
|
|
self.assertNotIn("install_recommends", args)
|
|
|
|
|
|
class TestBecomeEscalation(unittest.TestCase):
|
|
def setUp(self):
|
|
self.become = become_loader.get("sudo")
|
|
self.become.set_options(direct={"become_user": "root"})
|
|
self.shell = shell_loader.get("sh")
|
|
self.commands = []
|
|
|
|
def _action(self, become):
|
|
action = _action({})
|
|
action._connection = mock.Mock(become=become, transport="local")
|
|
action._execute_module = mock.Mock(side_effect=self._record)
|
|
return action
|
|
|
|
def _record(self, **_kwargs):
|
|
self.commands.append(self.become.build_become_command("MODULE", self.shell))
|
|
return {"changed": True}
|
|
|
|
def test_the_build_user_call_is_wrapped(self):
|
|
action = self._action(self.become)
|
|
action._execute(ModuleCall("m", {}, become_user="aur_builder"), {}, {})
|
|
self.assertIn("-u aur_builder", self.commands[0])
|
|
|
|
def test_a_plain_call_is_not_wrapped(self):
|
|
action = self._action(self.become)
|
|
action._execute(ModuleCall("m", {}), {}, {})
|
|
self.assertNotIn("-u aur_builder", self.commands[0])
|
|
self.assertIn("-u root", self.commands[0])
|
|
|
|
def test_the_become_user_is_restored(self):
|
|
action = self._action(self.become)
|
|
action._execute(ModuleCall("m", {}, become_user="aur_builder"), {}, {})
|
|
self.assertEqual(self.become.get_option("become_user"), "root")
|
|
|
|
def test_a_later_call_on_the_same_connection_is_unaffected(self):
|
|
action = self._action(self.become)
|
|
action._execute(ModuleCall("m", {}, become_user="aur_builder"), {}, {})
|
|
action._execute(ModuleCall("m", {}), {}, {})
|
|
self.assertIn("-u aur_builder", self.commands[0])
|
|
self.assertIn("-u root", self.commands[1])
|
|
self.assertNotIn("-u aur_builder", self.commands[1])
|
|
|
|
def test_the_become_user_is_restored_after_a_failing_call(self):
|
|
action = self._action(self.become)
|
|
action._execute_module.side_effect = RuntimeError("boom")
|
|
with self.assertRaises(RuntimeError):
|
|
action._execute(ModuleCall("m", {}, become_user="aur_builder"), {}, {})
|
|
self.assertEqual(self.become.get_option("become_user"), "root")
|
|
|
|
def test_without_a_become_plugin_it_fails_loud(self):
|
|
action = self._action(None)
|
|
with self.assertRaises(AnsibleActionFail) as caught:
|
|
action._execute(ModuleCall("m", {}, become_user="aur_builder"), {}, {})
|
|
self.assertIn("aur_builder", str(caught.exception))
|
|
|
|
|
|
class TestTargetFacts(unittest.TestCase):
|
|
"""A delegated install must resolve against the delegation target.
|
|
|
|
task_vars carries the inventory host's facts even under delegate_to, so a
|
|
RedHat node delegating to a Debian controller used to resolve dnf and then
|
|
run it where only apt exists.
|
|
"""
|
|
|
|
def _action(self, delegate_to, setup_result=None):
|
|
action = _action({"id": "nfs-client"})
|
|
action._task.delegate_to = delegate_to
|
|
action._execute_module = mock.Mock(return_value=setup_result or {})
|
|
return action
|
|
|
|
def test_without_delegation_the_inventory_facts_are_used(self):
|
|
action = self._action(None)
|
|
facts = action._target_facts({"ansible_facts": {"pkg_mgr": "apt"}})
|
|
self.assertEqual(facts, {"pkg_mgr": "apt"})
|
|
action._execute_module.assert_not_called()
|
|
|
|
def test_delegating_to_itself_is_not_delegation(self):
|
|
action = self._action("nfs-node")
|
|
facts = action._target_facts(
|
|
{"inventory_hostname": "nfs-node", "ansible_facts": {"pkg_mgr": "dnf"}}
|
|
)
|
|
self.assertEqual(facts, {"pkg_mgr": "dnf"})
|
|
action._execute_module.assert_not_called()
|
|
|
|
def test_a_delegated_task_asks_the_target(self):
|
|
action = self._action(
|
|
"localhost",
|
|
{
|
|
"ansible_facts": {
|
|
"ansible_pkg_mgr": "apt",
|
|
"ansible_os_family": "Debian",
|
|
}
|
|
},
|
|
)
|
|
facts = action._target_facts(
|
|
{
|
|
"inventory_hostname": "nfs-node",
|
|
"ansible_facts": {"pkg_mgr": "dnf", "os_family": "RedHat"},
|
|
}
|
|
)
|
|
self.assertEqual(
|
|
action._module_name(ModuleCall(GENERIC_PACKAGE, {}), facts), "apt"
|
|
)
|
|
|
|
def test_a_failing_setup_names_the_delegation_target(self):
|
|
action = self._action("localhost", {"failed": True, "msg": "no connection"})
|
|
with self.assertRaises(AnsibleActionFail) as caught:
|
|
action._target_facts({"inventory_hostname": "nfs-node"})
|
|
self.assertIn("localhost", str(caught.exception))
|
|
|
|
|
|
class TestAttempt(unittest.TestCase):
|
|
def _action_with(self, results):
|
|
action = _action({})
|
|
action._execute = mock.Mock(side_effect=list(results))
|
|
return action
|
|
|
|
def test_without_a_policy_the_call_runs_once(self):
|
|
action = self._action_with([{"failed": True}])
|
|
result = action._attempt(ModuleCall("m", {}), {}, {})
|
|
self.assertEqual(action._execute.call_count, 1)
|
|
self.assertEqual(result["module"], "m")
|
|
|
|
def test_a_policy_repeats_until_the_budget_is_spent(self):
|
|
action = self._action_with([{"failed": True}] * 3)
|
|
call = ModuleCall("m", {}, retry=RetryPolicy(attempts=3, seconds=0))
|
|
result = action._attempt(call, {}, {})
|
|
self.assertEqual(action._execute.call_count, 3)
|
|
self.assertTrue(result["failed"])
|
|
|
|
def test_a_policy_stops_at_the_first_success(self):
|
|
action = self._action_with([{"failed": True}, {"changed": True}])
|
|
call = ModuleCall("m", {}, retry=RetryPolicy(attempts=3, seconds=0))
|
|
result = action._attempt(call, {}, {})
|
|
self.assertEqual(action._execute.call_count, 2)
|
|
self.assertFalse(result.get("failed"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|