diff --git a/plugins/module_utils/network/vyos/config/vrrp/vrrp.py b/plugins/module_utils/network/vyos/config/vrrp/vrrp.py index 5630738f..161bd4fb 100644 --- a/plugins/module_utils/network/vyos/config/vrrp/vrrp.py +++ b/plugins/module_utils/network/vyos/config/vrrp/vrrp.py @@ -1,644 +1,658 @@ # # -*- coding: utf-8 -*- # Copyright 2021 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # from __future__ import absolute_import, division, print_function __metaclass__ = type """ The vyos_vrrp config file. It is in this file where the current configuration (as dict) is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to its desired end-state is created. """ from copy import deepcopy from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( remove_empties, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrrp import ( VrrpTemplate, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( LooseVersion, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Vrrp(ResourceModule): """ The vyos_vrrp config class """ def __init__(self, module): super(Vrrp, self).__init__( empty_fact_val={}, facts_module=Facts(module), module=module, resource="vrrp", tmplt=VrrpTemplate(), ) self.parsers = [ "disable", ] def _validate_template(self): version = get_os_version(self._module) if LooseVersion(version) >= LooseVersion("1.4"): self._tmplt = VrrpTemplate() else: self._module.fail_json(msg="VRRP is not supported in this version of VyOS") def parse(self): """override parse to check template""" self._validate_template() return super().parse() def get_parser(self, name): """get_parsers""" self._validate_template() return super().get_parser(name) def execute_module(self): """Execute the module :rtype: A dictionary :returns: The result from module execution """ if self.state not in ["parsed", "gathered", "purged"]: self.generate_commands() self.run_commands() if self.state == "purged": - wantd = {"disable": False, "vrrp": {"snmp": "disabled"}} + wantd = {"disable": False} haved = deepcopy(self.have) - if wantd != haved: self.commands = ["delete high-availability"] self.run_commands() return self.result def generate_commands(self): """Generate configuration commands to send based on want, have and desired state. """ wantd = {} haved = {} wantd = deepcopy(self.want) haved = deepcopy(self.have) # self._module.fail_json(msg="Generate commands - want: " + str(self.want) + " (((()))) have: " + str(haved)) for entry in wantd, haved: # self._module.fail_json(msg="Before normalize_vrrp_groups - entry: " + str(entry)) self._vrrp_groups_list_to_dict(entry) self._virtual_servers_list_to_dict(entry) self._vrrp_sync_groups_list_to_dict(entry) self._normalize_lists(entry) # self._module.fail_json(msg="Normalise - want: " + str(wantd) + " (((()))) have: " + str(haved)) if self.state in ["overridden", "deleted"]: - # self._module.fail_json(msg=self._module.params.get('config', {})) wantd, haved, p = self._prune_stubs(self._module.params.get("config", {}), haved) + # self._module.fail_json(msg=wantd) keys = set(wantd) | set(haved) for k in keys: want = wantd.get(k, {}) have = haved.get(k, {}) if k == "vrrp": if self.state in ["merged"]: want = combine(have, want, recursive=True) self._compare_vrrp(want, have) if k == "virtual_servers": if self.state in ["merged"]: want = combine(have, want, recursive=True) self._compare_vsrvs(want, have) if self.state in ["deleted"] and k == "disable": want = have if self.state in ["rendered"]: have = None self.compare( parsers=self.parsers, want={k: want}, have={k: have}, ) self.commands = sorted(list(dict.fromkeys(self.commands))) # self._module.fail_json(msg=self.commands) def _compare_vsrvs(self, want, have): """Compare virtual servers of VRRP""" vs_parsers = [ "virtual_servers.address", "virtual_servers.algorithm", "virtual_servers.delay_loop", "virtual_servers.forward_method", "virtual_servers.persistence_timeout", "virtual_servers.fwmark", "virtual_servers.port", "virtual_servers.protocol", "virtual_servers.real_server.port", "virtual_servers.real_server.health_check_script", "virtual_servers.real_server.connection_timeout", ] pairs = [] # self._module.fail_json(msg="Want: " + str(want) + "&&&&&&&&&&&&&&&&&&&& have: " + str(have)) hlist = self._extract_named_leafs(have) wlist = self._extract_named_leafs(want) if self.state == "rendered": hlist = [] # self._module.fail_json(msg="Want: " + str(wlist) + "&&&&&&&&&&&&&&&&&&&& have: " + str(hlist)) if self.state in ["replaced", "deleted", "overridden"]: for hdict in hlist: wdict = self._find_matching_vsrv(hdict, wlist) if self.state == "deleted" and wdict: wdict = {} elif not wdict: hdict = {} pairs.append((wdict, hdict)) self.compare( parsers=vs_parsers, want={"virtual_servers": wdict}, have={"virtual_servers": hdict}, ) # self._module.fail_json(msg=pairs) if self.state in ["merged", "replaced", "rendered"]: for wdict in wlist: hdict = self._find_matching_vsrv(wdict, hlist) pairs.append((wdict, hdict)) self.compare( parsers=vs_parsers, want={"virtual_servers": wdict}, have={"virtual_servers": hdict}, ) # self._module.fail_json(msg=pairs) def _compare_vrrp(self, want, have): """Compare the instances of VRRP""" vrrp_parsers = [ "vrrp.snmp", "vrrp.global_parameters", "vrrp.global_parameters.garp", "vrrp.groups", "vrrp.groups.disable", "vrrp.groups.no_preempt", "vrrp.groups.rfc3768_compatibility", "vrrp.groups.excluded_address", "vrrp.groups.garp", "vrrp.groups.authentication", "vrrp.groups.transition_script", "vrrp.groups.health_check", "vrrp.groups.track.interface", "vrrp.groups.track.exclude_vrrp_interface", "vrrp.sync_groups.member", "vrrp.sync_groups.transition_script", "vrrp.sync_groups.health_check", ] pairs = [] # self._module.fail_json(msg="Compare VRRP - want: " + str(want) + " (((()))) have: " + str(have)) if ( have.get("snmp") == "enabled" and want.get("snmp") != "enabled" and self.state not in ["deleted", "overridden"] ): - self.commands.append("delete high-availability vrrp 1 snmp") + self.commands.append("delete high-availability vrrp snmp") hlist = self._extract_leaf_items(have) wlist = self._extract_leaf_items(want) if self.state == "rendered": hlist = [] # self._module.fail_json(msg="leaf VRRP - want: " + str(wlist) + " (((()))) have: " + str(hlist)) # self.compare(parsers=vrrp_parsers, want={}, have={"vrrp": have}) if self.state in ["replaced", "deleted", "overridden"]: for hdict in hlist: wdict = self._find_matching_vrrp(hdict, wlist) - # self._module.fail_json(msg=wdict) if self.state == "deleted" and wdict: - self._module.fail_json(msg="Want: " + str(wdict)) wdict = {} elif not wdict: hdict = {} - # self._module.fail_json(msg="Want: " + str(wdict)) + pairs.append((wdict, hdict)) self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) # self._module.fail_json(msg=pairs) if self.state in ["merged", "replaced", "rendered"]: for wdict in wlist: hdict = self._find_matching_vrrp(wdict, hlist) pairs.append((wdict, hdict)) self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) # self._module.fail_json(msg=pairs) def _vrrp_groups_list_to_dict(self, data): vrrp = data.get("vrrp", {}) groups = vrrp.get("groups") if not groups: return data if isinstance(groups, dict): return data if isinstance(groups, list): new_groups = {} for item in groups: name = item.get("name") if not name: continue new_groups[name] = item data["vrrp"]["groups"] = new_groups return data return data def _vrrp_sync_groups_list_to_dict(self, data): vrrp = data.get("vrrp", {}) groups = vrrp.get("sync_groups") if not groups: return data if isinstance(groups, dict): return data if isinstance(groups, list): new_groups = {} for item in groups: name = item.get("name") if not name: continue new_groups[name] = item data["vrrp"]["sync_groups"] = new_groups return data return data def _virtual_servers_list_to_dict(self, data): vss = data.get("virtual_servers") if not vss: return data if isinstance(vss, dict): for vs in vss.items(): rs = vs.get("real_server") if isinstance(rs, list): vs["real_server"] = { item["address"]: item for item in rs if isinstance(item, dict) and item.get("address") } return data if isinstance(vss, list): new_vss = {} for vs in vss: if not isinstance(vs, dict): continue name = vs.get("name") if not name: continue rs = vs.get("real_server") if isinstance(rs, list): vs["real_server"] = { item["address"]: item for item in rs if isinstance(item, dict) and item.get("address") } new_vss[name] = vs data["virtual_servers"] = new_vss return data return data def _extract_leaf_items(self, data, path=None, parent_name=None): path = path or [] results = [] if isinstance(data, dict): current_name = data.get("name", parent_name) for k, v in data.items(): if k == "name" or (k == "snmp" and v == "disabled"): continue results.extend(self._extract_leaf_items(v, path + [k], current_name)) return results leaf_key = path[-1] top_key = path[0] if top_key in ["groups", "sync_groups"]: subkeys = path[2:] else: subkeys = path[1:] nested = {leaf_key: data} for p in reversed(subkeys[:-1]): nested = {p: nested} if parent_name: out = {top_key: {"name": parent_name}} out[top_key].update(nested) else: out = {top_key: nested} results.append(out) return results def _find_matching_vsrv(self, want_item, have_list): def build_sig(item): if not isinstance(item, dict): return None name = item.get("name") if not name: return None # --- real_server case --- if "real_server" in item: rs = item["real_server"] if not isinstance(rs, dict) or "address" not in rs: return None addr = rs["address"] # attribute path under real_server for k in rs: if k != "address": return ("real_server", name, addr, k) # address-only (rare but valid) return ("real_server", name, addr, None) # --- flat attribute --- for k in item: if k != "name": return ("attr", name, k) return None sig_want = build_sig(want_item) for obj in have_list: if build_sig(obj) == sig_want: return obj return {} def _find_matching_vrrp(self, want_item, have_list): def build_sig(item): if not isinstance(item, dict) or not item: return () container = next(iter(item)) inner = item[container] sig = [container] # identity (group / sync_group name) if isinstance(inner, dict) and "name" in inner: sig.append(("name", inner["name"])) # find the real leaf if isinstance(inner, dict): for k, v in inner.items(): if k == "name": continue # flat leaf (e.g. vrid, address) if not isinstance(v, dict): sig.append(k) break # nested leaf (e.g. health_check.interval) sig.append(k) for leaf in v: sig.append(leaf) break break return tuple(sig) sig_want = build_sig(want_item) for obj in have_list: if build_sig(obj) == sig_want: return obj return {} def _normalize_lists(self, node): """ Recursively normalize all lists inside a dict or list. All lists are sorted to ensure consistent ordering for comparison. """ if isinstance(node, dict): for k, v in node.items(): if isinstance(v, list): # sort the list if it contains only scalars if all(not isinstance(i, (dict, list)) for i in v): node[k] = sorted(v) else: # recurse into each item if the list contains dicts for item in v: self._normalize_lists(item) elif isinstance(v, dict): self._normalize_lists(v) elif isinstance(node, list): for item in node: self._normalize_lists(item) def project_structure(self, have, want): """ Project the structure of `have` onto `want`. - Existing values in `want` are preserved - Missing paths are created with empty values """ if not isinstance(have, dict): return want if want is None or not isinstance(want, dict): want = {} for key, have_val in have.items(): if key not in want: want[key] = self.empty_like(have_val) else: want[key] = self.project_structure(have_val, want[key]) return want def empty_like(self, value): if isinstance(value, dict): return {} if isinstance(value, list): return [] return None def _extract_named_leafs(self, data, parent_name=None, prefix_key=None): results = [] if prefix_key == "real_server" and isinstance(data, dict): for d, server_data in data.items(): if not isinstance(server_data, dict): continue address = server_data.get("address") if not address: continue for k, v in server_data.items(): if k == "address": continue results.append( { "name": parent_name, "real_server": { "address": address, k: v, }, }, ) return results # Generic dict handling if isinstance(data, dict): current_name = data.get("name", parent_name) for k, v in data.items(): if k == "name": continue results.extend( self._extract_named_leafs(v, current_name, k), ) return results # Primitive leaf return [ { "name": parent_name, prefix_key: data, }, ] def _prune_stubs(self, w, h, path=""): wc = {} hc = self._remove_defaults(h) - if not w and remove_empties(hc): + if not self._remove_nulls(w) and remove_empties(hc): self.commands = ["delete high-availability"] return {}, {}, path for k, wg in (self._remove_nulls(w) or {}).items(): next_path = f"{path} {k}".strip() stub = self._cli_path(next_path) hg = remove_empties(hc).get(k) if hg is None: continue if not isinstance(wg, (dict, list)): self.commands.append(f"delete high-availability {stub}") hc.pop(k, None) wc.pop(k, None) continue if not wg: self.commands.append(f"delete high-availability {stub}") hc.pop(k, None) wc.pop(k, None) continue if isinstance(wg, list) and isinstance(hg, dict): for item in wg: name = item.get("name") if not name: continue if name in hg: self.commands.append( f"delete high-availability {stub} {name}", ) - hc.pop(k, None) - wc.pop(k, None) + hg.pop(name, None) + + if hg: + hc[k] = hg + else: + hc.pop(k, None) + + if self._remove_nulls(wg): + wc[k] = wg + # self._module.fail_json(msg=str(wc)) + else: + wc.pop(k, None) + + # hc.pop(k, None) + # wc.pop(k, None) continue if isinstance(wg, dict) and isinstance(hg, dict): wi, hi, p = self._prune_stubs(wg, hg, next_path) if wi: wc[k] = wi - hc.pop(k, None) + if hi: + hc[k] = hi + else: + hc.pop(k, None) + # hc.pop(k, None) return wc, hc, path def _remove_nulls(self, data): """Remove null values but keep empty containers.""" if isinstance(data, dict): # Remove None values, keep empty dicts cleaned = {} for k, v in data.items(): if v in [None, "disabled", False]: continue # Skip null values cleaned[k] = self._remove_nulls(v) return cleaned return data def _remove_defaults(self, data): if isinstance(data, dict): cleaned = {} for k, v in data.items(): if v in [None, False, "disabled"]: continue v = self._remove_defaults(v) cleaned[k] = v return cleaned return data def _cli_path(self, path): token_map = { "groups": "group", "sync_groups": "sync-group", "virtual_servers": "virtual-server", } parts = [] for p in path.split(): p = token_map.get(p, p) parts.append(p.replace("_", "-")) return " ".join(parts) diff --git a/tests/integration/targets/vyos_vrrp/tests/cli/purged.yaml b/tests/integration/targets/vyos_vrrp/tests/cli/purged.yaml index 51ea21b0..2cbacb65 100644 --- a/tests/integration/targets/vyos_vrrp/tests/cli/purged.yaml +++ b/tests/integration/targets/vyos_vrrp/tests/cli/purged.yaml @@ -1,44 +1,44 @@ --- - debug: msg: Start vyos_vrrp purged / deleted_all integration tests ansible_connection={{ ansible_connection }} - include_tasks: _populate_config.yaml - block: - name: Purge all the VRRP configuration. register: result vyos.vyos.vyos_vrrp: &id001 config: state: purged - name: Assert that the before dicts were correctly generated assert: that: - "{{ merged.before | symmetric_difference(result['before']) |length == 0 }}" - name: Assert that the correct set of commands were generated assert: that: - "{{ purged.commands | symmetric_difference(result['commands']) |length == 0 }}" - name: Assert that the after dicts were correctly generated assert: that: - "{{ purged.after | symmetric_difference(result['after']) |length == 0 }}" - - name: Delete attributes of given interfaces (IDEMPOTENT) + - name: Purge all the VRRP configuration (IDEMPOTENT) register: result vyos.vyos.vyos_vrrp: *id001 - name: Assert that the previous task was idempotent assert: that: - result.changed == false - result.commands|length == 0 - name: Assert that the before dicts were correctly generated assert: that: - "{{ purged.after | symmetric_difference(result['before']) |length == 0 }}" always: - include_tasks: _remove_config.yaml diff --git a/tests/unit/modules/network/vyos/test_vyos_vrrp.py b/tests/unit/modules/network/vyos/test_vyos_vrrp.py index 640d4396..311a1d68 100644 --- a/tests/unit/modules/network/vyos/test_vyos_vrrp.py +++ b/tests/unit/modules/network/vyos/test_vyos_vrrp.py @@ -1,1118 +1,958 @@ # (c) 2021 Red Hat Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # Make coding more python3-ish from __future__ import absolute_import, division, print_function __metaclass__ = type from unittest.mock import patch from ansible_collections.vyos.vyos.plugins.modules import vyos_vrrp from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture class TestVyosVrrpModule(TestVyosModule): module = vyos_vrrp def setUp(self): super(TestVyosVrrpModule, self).setUp() self.mock_get_resource_connection_config = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection", ) self.get_resource_connection_config = self.mock_get_resource_connection_config.start() self.mock_get_resource_connection_facts = patch( "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", ) self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() self.mock_execute_show_command = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrrp.vrrp.VrrpFacts.get_config", ) self.execute_show_command = self.mock_execute_show_command.start() self.mock_get_os_version = patch( "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vrrp.vrrp.get_os_version", ) self.get_os_version = self.mock_get_os_version.start() self.get_os_version.return_value = "1.5" self.maxDiff = None def tearDown(self): super(TestVyosVrrpModule, self).tearDown() self.mock_get_resource_connection_config.stop() self.mock_get_resource_connection_facts.stop() self.mock_execute_show_command.stop() self.mock_get_os_version.stop() def load_fixtures(self, commands=None, filename=None): if filename is None: filename = "vyos_vrrp_config.cfg" def load_from_file(*args, **kwargs): output = load_fixture(filename) return output self.execute_show_command.side_effect = load_from_file def test_vrrp_merged_idempotent(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, name="s1", persistence_timeout="30", protocol="tcp", real_server=[ dict( address="10.10.50.1", port=443, health_check_script="/var/tmp/script.sh", ), ], ), dict( address="10.10.10.2", name="s2", port=81, real_server=[ dict( address="real1", port=8081, connection_timeout=5, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( global_parameters=dict( garp=dict( master_refresh=100, ), version="3", ), groups=[ dict( authentication=dict( password="testpass", type="plaintext-password", ), address="1.1.1.1", advertise_interval=10, description="Group_1", disable=True, excluded_address=[ "192.168.1.8", "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh_repeat=100, master_repeat=3, ), health_check=dict( failure_count=3, interval=10, ping="192.168.1.5", script="script.sh", ), hello_source_address="192.168.1.2", interface="eth2", name="g1", no_preempt=True, peer_address="192.168.1.3", priority=100, rfc3768_compatibility=True, track=dict( exclude_vrrp_interface=True, interface=["eth1"], ), transition_script=dict( backup="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), vrid=20, ), ], snmp="enabled", sync_groups=[ dict( health_check=dict( failure_count=3, interval=10, ), member=["g1"], name="sg1", transition_script=dict( backup="/var/tmp/script.sh", master="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), ), ], ), ), state="merged", ), ) self.execute_module(changed=False, commands=[]) def test_vrrp_merged(self): set_module_args( dict( config=dict( disable=True, virtual_servers=[ dict( name="s1", address="10.10.10.5", algorithm="round-robin", real_server=[ dict( address="10.10.50.2", port=8443, ), ], ), dict( name="s2", address="10.10.10.2", persistence_timeout=30, port=81, protocol="tcp", ), dict( name="s3", address="10.10.10.3", port=88, protocol="udp", ), ], vrrp=dict( snmp="disabled", global_parameters=dict( startup_delay=35, garp=dict( master_repeat=6, ), ), groups=[ dict( name="g1", peer_address="192.168.1.3", priority=100, disable=False, no_preempt=False, ), ], sync_groups=[ dict( name="sg1", health_check=dict( failure_count=5, ), ), ], ), ), state="merged", ), ) commands = [ "delete high-availability vrrp group g1 no-preempt", "delete high-availability vrrp group g1 disable", "delete high-availability vrrp group g1 rfc3768-compatibility", "delete high-availability vrrp snmp", "set high-availability virtual-server s1 address 10.10.10.5", "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443", "set high-availability virtual-server s2 persistence-timeout 30", "set high-availability virtual-server s2 protocol tcp", "set high-availability virtual-server s3 address 10.10.10.3", "set high-availability virtual-server s3 port 88", "set high-availability virtual-server s3 protocol udp", "set high-availability vrrp global-parameters garp master-repeat 6", "set high-availability vrrp global-parameters startup-delay 35", "set high-availability vrrp sync-group sg1 health-check failure-count 5", ] self.execute_module(changed=True, commands=commands) # def test_ntp_replaced(self): # set_module_args( # dict( # config=dict( # allow_clients=["10.3.4.0/24", "10.4.5.0/24"], # listen_addresses=["10.3.3.1", "10.4.4.1"], # servers=[ # dict(server="server4", options=["noselect", "prefer"]), # dict( # server="server6", # options=[ # "noselect", # "dynamic", # "prefer", # "preempt", # ], # ), # dict(server="time1.vyos.net"), # dict(server="time2.vyos.net"), # dict(server="time3.vyos.net"), # ], # ), # state="replaced", # ), # ) # commands = [ # "delete system ntp allow-clients address 10.1.1.0/24", # "delete system ntp allow-clients address 10.1.2.0/24", # "delete system ntp listen-address 10.2.3.1", # "delete system ntp listen-address 10.4.3.1", # "delete system ntp server server1", # "delete system ntp server server3", # "set system ntp allow-clients address 10.3.4.0/24", # "set system ntp allow-clients address 10.4.5.0/24", # "set system ntp listen-address 10.3.3.1", # "set system ntp listen-address 10.4.4.1", # "set system ntp server server4 noselect", # "set system ntp server server4 prefer", # "set system ntp server server6 noselect", # "set system ntp server server6 dynamic", # "set system ntp server server6 prefer", # "set system ntp server server6 preempt", # ] # self.execute_module(changed=True, commands=commands) # def test_ntp_replaced_idempotent(self): # set_module_args( # dict( # config=dict( # allow_clients=["10.1.1.0/24", "10.1.2.0/24"], # listen_addresses=["10.2.3.1", "10.4.3.1"], # servers=[ # dict(server="server1"), # dict(server="server3", options=["noselect", "dynamic"]), # dict(server="time1.vyos.net"), # dict(server="time2.vyos.net"), # dict(server="time3.vyos.net"), # ], # ), # state="replaced", # ), # ) # self.execute_module(changed=False, commands=[]) # def test_ntp_overridden(self): # set_module_args( # dict( # config=dict( # allow_clients=["10.9.9.0/24"], # listen_addresses=["10.9.9.1"], # servers=[ # dict(server="server9"), # dict(server="server6", options=["noselect", "dynamic"]), # dict(server="time1.vyos.net"), # dict(server="time2.vyos.net"), # dict(server="time3.vyos.net"), # ], # ), # state="overridden", # ), # ) # commands = [ # "delete system ntp allow-clients address 10.1.1.0/24", # "delete system ntp allow-clients address 10.1.2.0/24", # "delete system ntp listen-address 10.2.3.1", # "delete system ntp listen-address 10.4.3.1", # "delete system ntp server server1", # "delete system ntp server server3", # "set system ntp allow-clients address 10.9.9.0/24", # "set system ntp listen-address 10.9.9.1", # "set system ntp server server9", # "set system ntp server server6 noselect", # "set system ntp server server6 dynamic", # ] # self.execute_module(changed=True, commands=commands) # def test_ntp_overridden_idempotent(self): # set_module_args( # dict( # config=dict( # allow_clients=["10.1.1.0/24", "10.1.2.0/24"], # listen_addresses=["10.2.3.1", "10.4.3.1"], # servers=[ # dict(server="server1"), # dict(server="server3", options=["noselect", "dynamic"]), # dict(server="time1.vyos.net"), # dict(server="time2.vyos.net"), # dict(server="time3.vyos.net"), # ], # ), # state="overridden", # ), # ) # self.execute_module(changed=False, commands=[]) def test_vrrp_rendered(self): set_module_args( dict( config=dict( virtual_servers=[ dict( name="s1", address="10.10.10.1", algorithm="round-robin", delay_loop=60, forward_method="direct", fwmark=10, persistence_timeout=30, protocol="tcp", real_server=[ dict( address="10.10.50.1", health_check_script="/var/tmp/script.sh", port=443, ), ], ), dict( name="s2", address="10.10.10.2", port=81, real_server=[ dict( address="real1", connection_timeout=5, port=8081, ), dict( address="real2", port=8080, ), ], ), ], vrrp=dict( snmp="enabled", global_parameters=dict( startup_delay=31, version="3", garp=dict( interval=30, master_delay=10, master_refresh=100, master_refresh_repeat=200, master_repeat=5, ), ), groups=[ dict( name="g1", description="Group_1", interface="eth2", address="1.1.1.1", advertise_interval=10, peer_address="192.168.1.3", priority=100, disable=True, no_preempt=True, rfc3768_compatibility=True, vrid=20, excluded_address=[ "192.168.1.7 interface eth3", ], garp=dict( interval=20, master_delay=5, master_refresh=50, master_refresh_repeat=100, master_repeat=3, ), authentication=dict( type="plaintext-password", password="testpass", ), transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), track=dict( exclude_vrrp_interface=True, interface=[ "eth0", ], ), ), dict( name="g2", description="Group_2", interface="eth1", address="2.2.2.2", disable=False, no_preempt=False, rfc3768_compatibility=False, vrid=11, health_check=dict( failure_count=5, interval=15, ping="192.168.1.100", script="/var/tmp/script.sh", ), hello_source_address="2.2.2.2", ), ], sync_groups=[ dict( name="sg1", member=[ "g1", ], transition_script=dict( master="/var/tmp/script.sh", backup="/var/tmp/script.sh", fault="/var/tmp/script.sh", stop="/var/tmp/script.sh", ), health_check=dict( failure_count=3, interval=10, ping="192.168.2.100", script="/var/tmp/script.sh", ), ), ], ), ), state="rendered", ), ) rendered_commands = [ "set high-availability virtual-server s1 address 10.10.10.1", "set high-availability virtual-server s1 algorithm round-robin", "set high-availability virtual-server s1 delay-loop 60", "set high-availability virtual-server s1 forward-method direct", "set high-availability virtual-server s1 fwmark 10", "set high-availability virtual-server s1 persistence-timeout 30", "set high-availability virtual-server s1 protocol tcp", "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script /var/tmp/script.sh", "set high-availability virtual-server s1 real-server 10.10.50.1 port 443", "set high-availability virtual-server s2 address 10.10.10.2", "set high-availability virtual-server s2 port 81", "set high-availability virtual-server s2 real-server real1 connection-timeout 5", "set high-availability virtual-server s2 real-server real1 port 8081", "set high-availability virtual-server s2 real-server real2 port 8080", "set high-availability vrrp global-parameters garp interval 30", "set high-availability vrrp global-parameters garp master-delay 10", "set high-availability vrrp global-parameters garp master-refresh 100", "set high-availability vrrp global-parameters garp master-refresh-repeat 200", "set high-availability vrrp global-parameters garp master-repeat 5", "set high-availability vrrp global-parameters startup-delay 31", "set high-availability vrrp global-parameters version 3", "set high-availability vrrp group g1 address 1.1.1.1", "set high-availability vrrp group g1 advertise-interval 10", "set high-availability vrrp group g1 authentication password testpass", "set high-availability vrrp group g1 authentication type plaintext-password", "set high-availability vrrp group g1 description 'Group_1'", "set high-availability vrrp group g1 disable", "set high-availability vrrp group g1 excluded-address 192.168.1.7 interface eth3", "set high-availability vrrp group g1 garp interval 20", "set high-availability vrrp group g1 garp master-delay 5", "set high-availability vrrp group g1 garp master-refresh 50", "set high-availability vrrp group g1 garp master-refresh-repeat 100", "set high-availability vrrp group g1 garp master-repeat 3", "set high-availability vrrp group g1 interface eth2", "set high-availability vrrp group g1 no-preempt", "set high-availability vrrp group g1 peer-address 192.168.1.3", "set high-availability vrrp group g1 priority 100", "set high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g1 track exclude-vrrp-interface", "set high-availability vrrp group g1 track interface eth0", "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh", "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh", "set high-availability vrrp group g1 vrid 20", "set high-availability vrrp group g2 address 2.2.2.2", "set high-availability vrrp group g2 description 'Group_2'", "set high-availability vrrp group g2 health-check failure-count 5", "set high-availability vrrp group g2 health-check interval 15", "set high-availability vrrp group g2 health-check ping 192.168.1.100", "set high-availability vrrp group g2 health-check script /var/tmp/script.sh", "set high-availability vrrp group g2 hello-source-address 2.2.2.2", "set high-availability vrrp group g2 interface eth1", "set high-availability vrrp group g2 vrid 11", "set high-availability vrrp snmp", "set high-availability vrrp sync-group sg1 health-check failure-count 3", "set high-availability vrrp sync-group sg1 health-check interval 10", "set high-availability vrrp sync-group sg1 health-check ping 192.168.2.100", "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 member g1", "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh", "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh", ] result = self.execute_module(changed=False) self.assertEqual( sorted(result["rendered"]), sorted(rendered_commands), result["rendered"], ) - # def test_vrrp_parsed(self): - # commands = ( - # "set high-availability virtual-server s1 address 10.10.10.1", - # "set high-availability virtual-server s1 algorithm round-robin", - # "set high-availability virtual-server s1 delay-loop 60", - # "set high-availability virtual-server s1 forward-method direct", - # "set high-availability virtual-server s1 fwmark 10", - # "set high-availability virtual-server s1 persistence-timeout 30", - # "set high-availability virtual-server s1 protocol tcp", - # "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script /var/tmp/script.sh", - # "set high-availability virtual-server s1 real-server 10.10.50.1 port 443", - # "set high-availability virtual-server s2 address 10.10.10.2", - # "set high-availability virtual-server s2 port 81", - # "set high-availability virtual-server s2 real-server real1 connection-timeout 5", - # "set high-availability virtual-server s2 real-server real1 port 8081", - # "set high-availability virtual-server s2 real-server real2 port 8080", - # "set high-availability vrrp global-parameters startup-delay 31", - # "set high-availability vrrp global-parameters version 3", - # "set high-availability vrrp global-parameters garp interval 30", - # "set high-availability vrrp global-parameters garp master-delay 10", - # "set high-availability vrrp global-parameters garp master-refresh 100", - # "set high-availability vrrp global-parameters garp master-refresh-repeat 200", - # "set high-availability vrrp global-parameters garp master-repeat 5", - # "set high-availability vrrp group g1 description 'Group_1'", - # "set high-availability vrrp group g1 disable", - # "set high-availability vrrp group g1 no-preempt", - # "set high-availability vrrp group g1 rfc3768-compatibility", - # "set high-availability vrrp group g1 address 1.1.1.1", - # "set high-availability vrrp group g1 advertise-interval 10", - # "set high-availability vrrp group g1 interface eth2", - # "set high-availability vrrp group g1 peer-address 192.168.1.3", - # "set high-availability vrrp group g1 priority 100", - # "set high-availability vrrp group g1 vrid 20", - # "set high-availability vrrp group g1 garp interval 20", - # "set high-availability vrrp group g1 garp master-delay 5", - # "set high-availability vrrp group g1 garp master-refresh 50", - # "set high-availability vrrp group g1 garp master-refresh-repeat 100", - # "set high-availability vrrp group g1 garp master-repeat 3", - # "set high-availability vrrp group g1 authentication password testpass", - # "set high-availability vrrp group g1 authentication type plaintext-password", - # "set high-availability vrrp group g1 transition-script backup /var/tmp/script.sh", - # "set high-availability vrrp group g1 transition-script fault /var/tmp/script.sh", - # "set high-availability vrrp group g1 transition-script master /var/tmp/script.sh", - # "set high-availability vrrp group g1 transition-script stop /var/tmp/script.sh", - # "set high-availability vrrp group g1 track exclude-vrrp-interface", - # "set high-availability vrrp group g1 track interface eth0", - # "set high-availability vrrp group g1 track interface eth3", - # "set high-availability vrrp group g1 excluded-address 192.168.1.7 interface eth3", - # "set high-availability vrrp sync-group sg1 health-check failure-count 3", - # "set high-availability vrrp sync-group sg1 health-check interval 10", - # "set high-availability vrrp sync-group sg1 health-check ping 192.168.2.100", - # "set high-availability vrrp sync-group sg1 health-check script /var/tmp/script.sh", - # "set high-availability vrrp sync-group sg1 member g1", - # "set high-availability vrrp sync-group sg1 transition-script backup /var/tmp/script.sh", - # "set high-availability vrrp sync-group sg1 transition-script fault /var/tmp/script.sh", - # "set high-availability vrrp sync-group sg1 transition-script master /var/tmp/script.sh", - # "set high-availability vrrp sync-group sg1 transition-script stop /var/tmp/script.sh", - # ) - # parsed_str = "\n".join(commands) - # set_module_args(dict(running_config=parsed_str, state="parsed")) - # result = self.execute_module(changed=False) - # parsed_list = { - # "disable": False, - # "virtual_servers": [ - # { - # "name": "s1", - # "address": "10.10.10.1", - # "algorithm": "round-robin", - # "delay_loop": 60, - # "forward_method": "direct", - # "fwmark": 10, - # "persistence_timeout": 30, - # "protocol": "tcp", - # "real_server": [ - # { - # "address": "10.10.50.1", - # "health_check_script": "/var/tmp/script.sh", - # "port": 443, - # } - # ], - # }, - # { - # "name": "s2", - # "address": "10.10.10.2", - # "port": 81, - # "real_server": [ - # { - # "address": "real1", - # "connection_timeout": 5, - # "port": 8081, - # }, - # { - # "address": "real2", - # "port": 8080, - # }, - # ], - # }, - # ], - # "vrrp": { - # "snmp": "disabled", - # "global_parameters": { - # "startup_delay": 31, - # "version": "3", - # "garp": { - # "interval": 30, - # "master_delay": 10, - # "master_refresh": 100, - # "master_refresh_repeat": 200, - # "master_repeat": 5, - # }, - # }, - # "groups": [ - # { - # "name": "g1", - # "description": "Group_1", - # "disable": True, - # "no_preempt": True, - # "rfc3768_compatibility": True, - # "interface": "eth2", - # "address": "1.1.1.1", - # "advertise_interval": 10, - # "peer_address": "192.168.1.3", - # "priority": 100, - # "vrid": 20, - # "garp": { - # "interval": 20, - # "master_delay": 5, - # "master_refresh": 50, - # "master_refresh_repeat": 100, - # "master_repeat": 3, - # }, - # "authentication": { - # "type": "plaintext-password", - # "password": "testpass", - # }, - # "transition_script": { - # "master": "/var/tmp/script.sh", - # "backup": "/var/tmp/script.sh", - # "fault": "/var/tmp/script.sh", - # "stop": "/var/tmp/script.sh", - # }, - # "track": { - # "exclude_vrrp_interface": True, - # "interface": ["eth0", "eth3"], - # }, - # "excluded_address": [ - # "192.168.1.7 interface eth3" - # ], - # }, - # ], - # "sync_groups": [ - # { - # "name": "sg1", - # "member": ["g1"], - # "transition_script": { - # "master": "/var/tmp/script.sh", - # "backup": "/var/tmp/script.sh", - # "fault": "/var/tmp/script.sh", - # "stop": "/var/tmp/script.sh", - # }, - # "health_check": { - # "failure_count": 3, - # "interval": 10, - # "ping": "192.168.2.100", - # "script": "/var/tmp/script.sh", - # }, - # } - # ], - # }, - # } - # self.assertEqual(parsed_list, result["parsed"]) - def test_vrrp_parsed(self): commands = ( "set high-availability disable", "set high-availability virtual-server s1 address 10.10.10.1", "set high-availability virtual-server s1 algorithm round-robin", "set high-availability virtual-server s1 delay-loop 60", "set high-availability virtual-server s1 forward-method direct", "set high-availability virtual-server s1 fwmark 10", "set high-availability virtual-server s1 persistence-timeout 30", "set high-availability virtual-server s1 protocol tcp", "set high-availability virtual-server s1 real-server 10.10.50.1 health-check script '/var/tmp/script.sh'", "set high-availability virtual-server s1 real-server 10.10.50.1 port 443", "set high-availability virtual-server s2 address 10.10.10.2", "set high-availability virtual-server s2 port 81", "set high-availability virtual-server s2 real-server real1 connection-timeout 5", "set high-availability virtual-server s2 real-server real1 port 8081", "set high-availability virtual-server s2 real-server real2 port 8080", "set high-availability vrrp global-parameters garp interval 30", "set high-availability vrrp global-parameters garp master-delay 10", "set high-availability vrrp global-parameters garp master-refresh 100", "set high-availability vrrp global-parameters garp master-refresh-repeat 200", "set high-availability vrrp global-parameters garp master-repeat 5", "set high-availability vrrp global-parameters startup-delay 30", "set high-availability vrrp global-parameters version 3", "set high-availability vrrp group g1 address '1.1.1.1'", "set high-availability vrrp group g1 advertise-interval 10", "set high-availability vrrp group g1 authentication password 'testpass'", "set high-availability vrrp group g1 authentication type 'plaintext-password'", "set high-availability vrrp group g1 description 'Group_1'", "set high-availability vrrp group g1 disable", "set high-availability vrrp group g1 excluded-address '192.168.1.7' interface 'eth3'", "set high-availability vrrp group g1 excluded-address '192.168.1.8'", "set high-availability vrrp group g1 garp interval 20", "set high-availability vrrp group g1 garp master-delay 5", "set high-availability vrrp group g1 garp master-refresh 50", "set high-availability vrrp group g1 garp master-refresh-repeat 100", "set high-availability vrrp group g1 garp master-repeat 3", "set high-availability vrrp group g1 health-check failure-count 3", "set high-availability vrrp group g1 health-check interval 10", "set high-availability vrrp group g1 health-check ping '192.168.1.5'", "set high-availability vrrp group g1 health-check script 'script.sh'", "set high-availability vrrp group g1 hello-source-address '192.168.1.2'", "set high-availability vrrp group g1 interface 'eth2'", "set high-availability vrrp group g1 no-preempt", "set high-availability vrrp group g1 peer-address '192.168.1.3'", "set high-availability vrrp group g1 priority 100", "set high-availability vrrp group g1 rfc3768-compatibility", "set high-availability vrrp group g1 track exclude-vrrp-interface", "set high-availability vrrp group g1 track interface 'eth1'", "set high-availability vrrp group g1 transition-script backup '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script fault '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script master '/var/tmp/script.sh'", "set high-availability vrrp group g1 transition-script stop '/var/tmp/script.sh'", "set high-availability vrrp group g1 vrid 20", "set high-availability vrrp snmp", "set high-availability vrrp sync-group sg1 health-check failure-count 3", "set high-availability vrrp sync-group sg1 health-check interval 10", "set high-availability vrrp sync-group sg1 health-check ping '192.168.1.1'", "set high-availability vrrp sync-group sg1 health-check script '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 member 'g1'", "set high-availability vrrp sync-group sg1 transition-script backup '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script fault '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script master '/var/tmp/script.sh'", "set high-availability vrrp sync-group sg1 transition-script stop '/var/tmp/script.sh'", ) parsed_str = "\n".join(commands) set_module_args(dict(running_config=parsed_str, state="parsed")) result = self.execute_module(changed=False) parsed_list = { "disable": True, "virtual_servers": [ { "name": "s1", "address": "10.10.10.1", "algorithm": "round-robin", "delay_loop": 60, "forward_method": "direct", "fwmark": 10, "persistence_timeout": 30, "protocol": "tcp", "real_server": [ { "address": "10.10.50.1", "health_check_script": "/var/tmp/script.sh", "port": 443, }, ], }, { "name": "s2", "address": "10.10.10.2", "port": 81, "real_server": [ { "address": "real1", "connection_timeout": 5, "port": 8081, }, { "address": "real2", "port": 8080, }, ], }, ], "vrrp": { "snmp": "enabled", "global_parameters": { "startup_delay": 30, "version": "3", "garp": { "interval": 30, "master_delay": 10, "master_refresh": 100, "master_refresh_repeat": 200, "master_repeat": 5, }, }, "groups": [ { "name": "g1", "description": "Group_1", "disable": True, "no_preempt": True, "rfc3768_compatibility": True, "interface": "eth2", "address": "1.1.1.1", "advertise_interval": 10, "peer_address": "192.168.1.3", "priority": 100, "vrid": 20, "garp": { "interval": 20, "master_delay": 5, "master_refresh": 50, "master_refresh_repeat": 100, "master_repeat": 3, }, "authentication": { "type": "plaintext-password", "password": "testpass", }, "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.5", "script": "script.sh", }, "track": { "exclude_vrrp_interface": True, "interface": ["eth1"], }, "excluded_address": [ "192.168.1.7 interface eth3", "192.168.1.8", ], "hello_source_address": "192.168.1.2", }, ], "sync_groups": [ { "name": "sg1", "member": ["g1"], "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.1", "script": "/var/tmp/script.sh", }, }, ], }, } self.assertEqual(parsed_list, result["parsed"]) def test_vrrp_gathered(self): set_module_args(dict(state="gathered")) result = self.execute_module(changed=False) gathered_list = { "disable": True, "virtual_servers": [ { "name": "s1", "address": "10.10.10.1", "algorithm": "round-robin", "delay_loop": 60, "forward_method": "direct", "fwmark": 10, "persistence_timeout": 30, "protocol": "tcp", "real_server": [ { "address": "10.10.50.1", "health_check_script": "/var/tmp/script.sh", "port": 443, }, ], }, { "name": "s2", "address": "10.10.10.2", "port": 81, "real_server": [ { "address": "real1", "connection_timeout": 5, "port": 8081, }, { "address": "real2", "port": 8080, }, ], }, ], "vrrp": { "snmp": "enabled", "global_parameters": { "startup_delay": 30, "version": "3", "garp": { "interval": 30, "master_delay": 10, "master_refresh": 100, "master_refresh_repeat": 200, "master_repeat": 5, }, }, "groups": [ { "name": "g1", "description": "Group_1", "disable": True, "no_preempt": True, "rfc3768_compatibility": True, "interface": "eth2", "address": "1.1.1.1", "advertise_interval": 10, "peer_address": "192.168.1.3", "priority": 100, "vrid": 20, "garp": { "interval": 20, "master_delay": 5, "master_refresh": 50, "master_refresh_repeat": 100, "master_repeat": 3, }, "authentication": { "type": "plaintext-password", "password": "testpass", }, "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.5", "script": "script.sh", }, "track": { "exclude_vrrp_interface": True, "interface": ["eth1"], }, "excluded_address": [ "192.168.1.7 interface eth3", "192.168.1.8", ], "hello_source_address": "192.168.1.2", }, ], "sync_groups": [ { "name": "sg1", "member": ["g1"], "transition_script": { "master": "/var/tmp/script.sh", "backup": "/var/tmp/script.sh", "fault": "/var/tmp/script.sh", "stop": "/var/tmp/script.sh", }, "health_check": { "failure_count": 3, "interval": 10, "ping": "192.168.1.1", "script": "/var/tmp/script.sh", }, }, ], }, } self.assertEqual(gathered_list, result["gathered"]) # def test_ntp_deleted(self): # # Delete the subsections that we include (listen_addresses and servers) # set_module_args( # dict( # config=dict( # allow_clients=["10.1.1.0/24"], # listen_addresses=["10.2.3.1"], # servers=[ # dict(server="server1"), # dict(server="server3", options=["noselect"]), # dict(server="time1.vyos.net"), # dict(server="time2.vyos.net"), # dict(server="time3.vyos.net"), # ], # ), # state="deleted", # ), # ) # commands = [ # "delete system ntp allow-clients", # 10.1.1.0/24", # "delete system ntp listen-address", # 10.2.3.1", # "delete system ntp server server1", # "delete system ntp server server3", # "delete system ntp server time1.vyos.net", # "delete system ntp server time2.vyos.net", # "delete system ntp server time3.vyos.net", # "delete system ntp", # ] # self.execute_module(changed=True, commands=commands) + def test_vrrp_deleted_all(self): + set_module_args( + dict( + config=dict(), + state="deleted", + ), + ) + commands = [ + "delete high-availability", + ] + self.execute_module(changed=True, commands=commands) + def test_vrrp_purged(self): set_module_args( dict( config=dict(), state="purged", ), ) commands = [ "delete high-availability", ] self.execute_module(changed=True, commands=commands)