summaryrefslogtreecommitdiff
path: root/tests/unit/modules/test_vyos_ha.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/unit/modules/test_vyos_ha.py')
-rw-r--r--tests/unit/modules/test_vyos_ha.py345
1 files changed, 345 insertions, 0 deletions
diff --git a/tests/unit/modules/test_vyos_ha.py b/tests/unit/modules/test_vyos_ha.py
new file mode 100644
index 0000000..10a9144
--- /dev/null
+++ b/tests/unit/modules/test_vyos_ha.py
@@ -0,0 +1,345 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import unittest
+
+from unittest.mock import MagicMock
+
+from ansible_collections.vyos.rest.plugins.modules.vyos_ha import (
+ _device_to_argspec,
+ _group_from_device,
+ _group_to_device,
+ _real_server_from_device,
+ _real_server_to_device,
+ _virtual_server_from_device,
+ _virtual_server_to_device,
+ _want_to_device,
+ build_commands,
+ get_running_config,
+)
+
+from .base import load_fixture
+
+
+_BASE = ["high-availability"]
+
+
+class VyOSModuleTestCase(unittest.TestCase):
+ def setUp(self):
+ self.mock_vyos = MagicMock()
+ self.fixture = load_fixture("ha_running.json")
+ self.mock_vyos.get_config = MagicMock(return_value=self.fixture)
+
+
+class TestGetRunningConfig(VyOSModuleTestCase):
+ def test_returns_raw_device_dict(self):
+ self.assertEqual(get_running_config(self.mock_vyos), self.fixture)
+
+ def test_empty_config(self):
+ self.mock_vyos.get_config = MagicMock(return_value=None)
+ self.assertEqual(get_running_config(self.mock_vyos), {})
+
+
+class TestRealServer(unittest.TestCase):
+ def test_to_device_generic_fields(self):
+ result = _real_server_to_device({"address": "10.0.0.2", "port": 8080})
+ self.assertEqual(result, {"port": 8080})
+
+ def test_to_device_health_check_script_nested(self):
+ """health_check_script is a genuine structural exception -- the
+ argspec has it flat, the device nests it under health-check.script."""
+ result = _real_server_to_device(
+ {"address": "10.0.0.2", "health_check_script": "/check.sh"},
+ )
+ self.assertEqual(result, {"health-check": {"script": "/check.sh"}})
+
+ def test_from_device_basic(self):
+ entry = _real_server_from_device("10.0.0.2", {"port": "8080"})
+ self.assertEqual(entry["address"], "10.0.0.2")
+ self.assertEqual(entry["port"], 8080)
+
+ def test_from_device_health_check_script_extracted(self):
+ entry = _real_server_from_device(
+ "10.0.0.2",
+ {"health-check": {"script": "/check.sh"}},
+ )
+ self.assertEqual(entry["health_check_script"], "/check.sh")
+
+
+class TestVirtualServer(unittest.TestCase):
+ def test_to_device_keyed_fields(self):
+ result = _virtual_server_to_device({"name": "s1", "address": "10.0.0.1", "port": 80})
+ self.assertEqual(result, {"address": "10.0.0.1", "port": 80})
+
+ def test_to_device_real_server_keyed_by_address(self):
+ vs = {
+ "name": "s1",
+ "port": 80,
+ "real_server": [{"address": "10.0.0.2", "port": 8080}],
+ }
+ result = _virtual_server_to_device(vs)
+ self.assertEqual(result["real-server"]["10.0.0.2"], {"port": 8080})
+
+ def test_from_device_list_with_real_servers(self):
+ entry = _virtual_server_from_device(
+ "s1",
+ {"port": "80", "real-server": {"10.0.0.2": {"port": "8080"}}},
+ )
+ self.assertEqual(entry["name"], "s1")
+ self.assertEqual(entry["port"], 80)
+ self.assertEqual(entry["real_server"][0]["address"], "10.0.0.2")
+ self.assertEqual(entry["real_server"][0]["port"], 8080)
+
+
+class TestGroup(unittest.TestCase):
+ """address/excluded_address are genuine tagNodes (confirmed); track
+ is NOT special-cased for interface since that's a plain list."""
+
+ def test_to_device_basic_fields_generic(self):
+ result = _group_to_device({"name": "g1", "vrid": 20, "interface": "eth0"})
+ self.assertEqual(result, {"vrid": 20, "interface": "eth0"})
+
+ def test_to_device_address_tag_node(self):
+ result = _group_to_device(
+ {"name": "g1", "address": ["192.168.1.1/24", "192.168.1.2/24"]},
+ )
+ self.assertEqual(
+ result["address"],
+ {"192.168.1.1/24": {}, "192.168.1.2/24": {}},
+ )
+
+ def test_to_device_excluded_address_tag_node(self):
+ result = _group_to_device({"name": "g1", "excluded_address": ["10.0.0.1"]})
+ self.assertEqual(result["excluded-address"], {"10.0.0.1": {}})
+
+ def test_to_device_track_interface_stays_plain_list(self):
+ """Regression test: track.interface is a confirmed <multi/>
+ leafNode, not a tag node -- must NOT be reshaped into a
+ dict-of-presence like address/excluded_address are."""
+ result = _group_to_device({"name": "g1", "track": {"interface": ["eth1", "eth2"]}})
+ self.assertEqual(result["track"]["interface"], ["eth1", "eth2"])
+
+ def test_to_device_bool_fields(self):
+ result = _group_to_device(
+ {"name": "g1", "disable": True, "no_preempt": True, "rfc3768_compatibility": False},
+ )
+ self.assertEqual(result["disable"], {})
+ self.assertEqual(result["no_preempt"], {})
+ self.assertNotIn("rfc3768_compatibility", result)
+
+ def test_from_device_vrid_and_priority_cast_to_int(self):
+ entry = _group_from_device("g1", {"vrid": "20", "priority": "100"})
+ self.assertEqual(entry["vrid"], 20)
+ self.assertEqual(entry["priority"], 100)
+
+ def test_from_device_address_tag_node_to_sorted_list(self):
+ entry = _group_from_device(
+ "g1",
+ {"address": {"192.168.1.2/24": {}, "192.168.1.1/24": {}}},
+ )
+ self.assertEqual(entry["address"], ["192.168.1.1/24", "192.168.1.2/24"])
+
+ def test_from_device_single_address_string_collapse(self):
+ entry = _group_from_device("g1", {"address": "192.168.1.1/24"})
+ self.assertEqual(entry["address"], ["192.168.1.1/24"])
+
+ def test_from_device_track_interface_stays_plain_list(self):
+ entry = _group_from_device("g1", {"track": {"interface": ["eth1", "eth2"]}})
+ self.assertEqual(entry["track"]["interface"], ["eth1", "eth2"])
+
+ def test_from_device_bool_presence_nodes(self):
+ entry = _group_from_device("g1", {"disable": {}, "no-preempt": {}})
+ self.assertTrue(entry["disable"])
+ self.assertTrue(entry["no_preempt"])
+
+
+class TestWantToDevice(unittest.TestCase):
+ def test_empty(self):
+ self.assertEqual(_want_to_device({}), {})
+ self.assertEqual(_want_to_device(None), {})
+
+ def test_disable(self):
+ result = _want_to_device({"disable": True})
+ self.assertEqual(result["disable"], {})
+
+ def test_virtual_server_keyed_by_name(self):
+ config = {"virtual_servers": [{"name": "s1", "address": "10.0.0.1", "port": 80}]}
+ result = _want_to_device(config)
+ self.assertIn("s1", result["virtual-server"])
+
+ def test_vrrp_global_parameters_generic(self):
+ config = {
+ "vrrp": {"global_parameters": {"startup_delay": 30, "garp": {"master_repeat": 6}}},
+ }
+ result = _want_to_device(config)
+ gp = result["vrrp"]["global_parameters"]
+ self.assertEqual(gp["startup_delay"], 30)
+ self.assertEqual(gp["garp"]["master_repeat"], 6)
+
+ def test_snmp_enabled_becomes_presence_node(self):
+ result = _want_to_device({"vrrp": {"snmp": "enabled"}})
+ self.assertEqual(result["vrrp"]["snmp"], {})
+
+ def test_snmp_disabled_not_in_want(self):
+ result = _want_to_device({"vrrp": {"snmp": "disabled"}})
+ self.assertNotIn("snmp", result.get("vrrp", {}))
+
+ def test_group_keyed_by_name(self):
+ config = {"vrrp": {"groups": [{"name": "g1", "vrid": 20, "interface": "eth0"}]}}
+ result = _want_to_device(config)
+ self.assertEqual(result["vrrp"]["group"]["g1"]["vrid"], 20)
+
+ def test_sync_group_member_stays_plain_list(self):
+ """Regression test: member is a confirmed <multi/> leafNode, not
+ a tag node -- must stay a plain list."""
+ config = {"vrrp": {"sync_groups": [{"name": "sg1", "member": ["g1", "g2"]}]}}
+ result = _want_to_device(config)
+ self.assertEqual(result["vrrp"]["sync-group"]["sg1"]["member"], ["g1", "g2"])
+
+
+class TestDeviceToArgspecFixture(VyOSModuleTestCase):
+ def test_disable_parsed(self):
+ result = _device_to_argspec(self.fixture)
+ self.assertTrue(result["disable"])
+
+ def test_virtual_server_parsed(self):
+ result = _device_to_argspec(self.fixture)
+ vs = result["virtual_servers"][0]
+ self.assertEqual(vs["name"], "s1")
+ self.assertEqual(vs["port"], 80)
+ self.assertEqual(vs["real_server"][0]["address"], "10.10.50.2")
+ self.assertEqual(vs["real_server"][0]["port"], 8443)
+
+ def test_global_parameters_parsed(self):
+ result = _device_to_argspec(self.fixture)
+ gp = result["vrrp"]["global_parameters"]
+ self.assertEqual(gp["startup_delay"], 30)
+ self.assertEqual(gp["garp"]["master_repeat"], 6)
+
+ def test_snmp_parsed(self):
+ result = _device_to_argspec(self.fixture)
+ self.assertEqual(result["vrrp"]["snmp"], "enabled")
+
+ def test_groups_parsed_with_track_interface_as_list(self):
+ result = _device_to_argspec(self.fixture)
+ groups = {g["name"]: g for g in result["vrrp"]["groups"]}
+ self.assertEqual(groups["g1"]["interface"], "eth0")
+ self.assertEqual(groups["g1"]["vrid"], 20)
+ self.assertIn("192.168.1.100/24", groups["g1"]["address"])
+ self.assertTrue(groups["g1"]["no_preempt"])
+ self.assertEqual(groups["g1"]["track"]["interface"], ["eth1", "eth2"])
+ # g2: single address string collapsed by device -> list
+ self.assertEqual(groups["g2"]["address"], ["192.168.2.100/24"])
+
+ def test_sync_group_parsed_member_as_list(self):
+ result = _device_to_argspec(self.fixture)
+ sg = result["vrrp"]["sync_groups"][0]
+ self.assertEqual(sg["name"], "sg1")
+ self.assertEqual(sg["member"], ["g1"])
+ self.assertEqual(sg["health_check"]["failure_count"], 5)
+ self.assertEqual(sg["health_check"]["ping"], "192.168.1.1")
+
+ def test_empty_config(self):
+ self.assertEqual(_device_to_argspec({}), {})
+ self.assertEqual(_device_to_argspec(None), {})
+
+
+class TestBuildCommands(VyOSModuleTestCase):
+ def test_merged_idempotent_against_own_fixture(self):
+ have = _device_to_argspec(self.fixture)
+ self.assertEqual(build_commands(have, self.fixture, "merged"), [])
+
+ def test_replaced_idempotent_against_own_fixture(self):
+ have = _device_to_argspec(self.fixture)
+ self.assertEqual(build_commands(have, self.fixture, "replaced"), [])
+
+ def test_overridden_idempotent_against_own_fixture(self):
+ """overridden is a single dict_op purge+set call (simplified from
+ the original manual section-scan loop -- confirmed identical
+ behavior before removing the loop)."""
+ have = _device_to_argspec(self.fixture)
+ self.assertEqual(build_commands(have, self.fixture, "overridden"), [])
+
+ def test_merged_adds_vrrp_group(self):
+ config = {
+ "vrrp": {
+ "groups": [{"name": "g3", "vrid": 30, "interface": "eth2", "priority": 100}],
+ },
+ }
+ cmds = build_commands(config, {}, "merged")
+ self.assertIn(("set", _BASE + ["vrrp", "group", "g3", "vrid", "30"]), cmds)
+ self.assertIn(("set", _BASE + ["vrrp", "group", "g3", "interface", "eth2"]), cmds)
+
+ def test_overridden_deletes_omitted_top_level_section(self):
+ raw_have = {"virtual-server": {"s1": {"port": "80"}}, "vrrp": {"group": {"g1": {}}}}
+ config = {"vrrp": {"groups": [{"name": "g1"}]}}
+ cmds = build_commands(config, raw_have, "overridden")
+ self.assertIn(("delete", _BASE + ["virtual-server"]), cmds)
+
+ def test_replaced_removes_stale_track_interface_member(self):
+ """Regression test for the dict_op purge list-value fix (this
+ session): track.interface being a plain list means removing a
+ member under 'replaced' relies on dict_op's list-purge handling."""
+ raw_have = {"vrrp": {"group": {"g1": {"track": {"interface": ["eth1", "eth2"]}}}}}
+ config = {"vrrp": {"groups": [{"name": "g1", "track": {"interface": ["eth1"]}}]}}
+ cmds = build_commands(config, raw_have, "replaced")
+ self.assertIn(
+ ("delete", _BASE + ["vrrp", "group", "g1", "track", "interface", "eth2"]),
+ cmds,
+ )
+
+ def test_replaced_removes_stale_sync_group_member(self):
+ raw_have = {"vrrp": {"sync-group": {"sg1": {"member": ["g1", "g2"]}}}}
+ config = {"vrrp": {"sync_groups": [{"name": "sg1", "member": ["g1"]}]}}
+ cmds = build_commands(config, raw_have, "replaced")
+ self.assertIn(
+ ("delete", _BASE + ["vrrp", "sync-group", "sg1", "member", "g2"]),
+ cmds,
+ )
+
+ def test_snmp_disabled_deletes_presence_node(self):
+ raw_have = {"vrrp": {"snmp": {}}}
+ config = {"vrrp": {"snmp": "disabled"}}
+ cmds = build_commands(config, raw_have, "merged")
+ self.assertIn(("delete", _BASE + ["vrrp", "snmp"]), cmds)
+
+ def test_deleted_no_have_is_noop(self):
+ self.assertEqual(build_commands({}, {}, "deleted"), [])
+
+ def test_deleted_with_have(self):
+ self.assertEqual(
+ build_commands({}, {"vrrp": {"group": {"g1": {}}}}, "deleted"),
+ [("delete", _BASE)],
+ )
+
+ def test_collapsed_track_interface_no_char_iteration_bug(self):
+ """A group with exactly one tracked interface, collapsed by the
+ device to a bare string, must not be iterated character-by-
+ character (dict_op's own list handling corrects this natively)."""
+ raw_have = {"vrrp": {"group": {"g1": {"track": {"interface": "eth1"}}}}}
+ config = {"vrrp": {"groups": [{"name": "g1", "track": {"interface": ["eth1"]}}]}}
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+
+ def test_collapsed_sync_group_member_no_char_iteration_bug(self):
+ raw_have = {"vrrp": {"sync-group": {"sg1": {"member": "g1"}}}}
+ config = {"vrrp": {"sync_groups": [{"name": "sg1", "member": ["g1"]}]}}
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+
+ def test_virtual_server_address_never_treated_as_tag_node(self):
+ """Regression test: virtual-server.<name>.address is a flat
+ scalar (the load-balancer bind address), unlike vrrp.group.
+ <name>.address which IS a genuine tag node (VRRP virtual IPs).
+ Same key name, different device shape depending on section --
+ a blanket key-name-based coercion previously corrupted this
+ into a spurious diff every single run."""
+ raw_have = {"virtual-server": {"s1": {"address": "10.10.10.5", "port": "80"}}}
+ config = {"virtual_servers": [{"name": "s1", "address": "10.10.10.5", "port": 80}]}
+ self.assertEqual(build_commands(config, raw_have, "merged"), [])
+ self.assertEqual(build_commands(config, raw_have, "replaced"), [])
+
+
+if __name__ == "__main__":
+ unittest.main()