diff options
Diffstat (limited to 'plugins')
| l--------- | plugins/action/ha.py | 1 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/argspec/ha/__init__.py | 0 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/argspec/ha/ha.py | 196 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/config/ha/__init__.py | 0 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/config/ha/ha.py | 705 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/facts/facts.py | 4 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/facts/ha/__init__.py | 0 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/facts/ha/ha.py | 174 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/rm_templates/ha.py | 1011 | ||||
| -rw-r--r-- | plugins/module_utils/network/vyos/utils/utils.py | 54 | ||||
| -rw-r--r-- | plugins/modules/vyos_ha.py | 1327 |
11 files changed, 3464 insertions, 8 deletions
diff --git a/plugins/action/ha.py b/plugins/action/ha.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/ha.py @@ -0,0 +1 @@ +vyos.py
\ No newline at end of file diff --git a/plugins/module_utils/network/vyos/argspec/ha/__init__.py b/plugins/module_utils/network/vyos/argspec/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/ha/ha.py b/plugins/module_utils/network/vyos/argspec/ha/ha.py new file mode 100644 index 00000000..4fe431cd --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/ha/ha.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_ha module +""" + + +class HaArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_ha module""" + + argument_spec = { + "config": { + "type": "dict", + "required": False, + "options": { + "disable": {"type": "bool", "default": False}, + "virtual_servers": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "str"}, + "algorithm": {"type": "str"}, + "delay_loop": {"type": "int"}, + "forward_method": {"type": "str", "choices": ["direct", "nat"]}, + "fwmark": {"type": "int"}, + "persistence_timeout": {"type": "int"}, + "port": {"type": "int"}, + "protocol": {"type": "str", "choices": ["tcp", "udp"]}, + "real_server": { + "type": "list", + "elements": "dict", + "options": { + "address": {"type": "str", "required": True}, + "port": {"type": "int"}, + "connection_timeout": {"type": "int"}, + "health_check_script": {"type": "str"}, + }, + }, + }, + }, + "vrrp": { + "type": "dict", + "options": { + "global_parameters": { + "type": "dict", + "options": { + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "startup_delay": {"type": "int"}, + "version": {"type": "str"}, + }, + }, + "groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "list", "elements": "str"}, + "advertise_interval": {"type": "int"}, + "authentication": { + "type": "dict", + "options": { + "password": {"type": "str", "no_log": True}, + "type": {"type": "str"}, + }, + }, + "description": {"type": "str"}, + "disable": {"type": "bool", "default": False}, + "excluded_address": {"type": "list", "elements": "str"}, + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "hello_source_address": {"type": "str"}, + "interface": {"type": "str"}, + "no_preempt": {"type": "bool", "default": False}, + "peer_address": {"type": "str"}, + "preempt_delay": {"type": "int"}, + "priority": {"type": "int"}, + "rfc3768_compatibility": {"type": "bool", "default": False}, + "track": { + "type": "dict", + "options": { + "exclude_vrrp_interface": {"type": "bool"}, + "interface": {"type": "list", "elements": "str"}, + }, + }, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + "vrid": {"type": "int", "required": False}, + }, + }, + "snmp": { + "type": "str", + "choices": ["disabled", "enabled"], + }, + "sync_groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "member": {"type": "list", "elements": "str"}, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + }, + }, + }, + }, + }, + }, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "purged", + "replaced", + "gathered", + "rendered", + "parsed", + "overridden", + ], + "default": "merged", + }, + "running_config": {"type": "str"}, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/ha/__init__.py b/plugins/module_utils/network/vyos/config/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/config/ha/ha.py b/plugins/module_utils/network/vyos/config/ha/ha.py new file mode 100644 index 00000000..7fc96a35 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/ha/ha.py @@ -0,0 +1,705 @@ +# +# -*- 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_ha 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.ha import ( + HaTemplate, +) +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 Ha(ResourceModule): + """ + The vyos_ha config class + """ + + def __init__(self, module): + super(Ha, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="ha", + tmplt=HaTemplate(), + ) + self.parsers = [ + "disable", + ] + + self._validate_template() + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = HaTemplate() + else: + self._module.fail_json(msg="High Availability is not supported in this version of VyOS") + + 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} + haved = deepcopy(self.have) + if wantd != haved: + self.commands = ["delete high-availability"] + self.run_commands() + if "before" in self.result: + self._normalize_lists(self.result["before"]) + if "after" in self.result: + self._normalize_lists(self.result["after"]) + if "parsed" in self.result: + self._normalize_lists(self.result["parsed"]) + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + for entry in wantd, haved: + self._list_to_named_dict(entry) + self._normalize_lists(entry) + + if self.state in ["deleted"]: + wantd, haved, p = self._prune_stubs(self._module.params.get("config", {}), haved) + + if self.state in ["overridden"]: + wo = deepcopy(wantd) + self._diff_w_h(wo, haved) + + haved_disable = haved.get("disable") + + for k1, v1 in wo.items(): + + if not isinstance(v1, dict): + continue + + for name, obj in v1.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {name: {}}}, haved) + haved = hi + + for k2, v2 in v1.items(): + if not isinstance(v2, dict): + continue + + for name, obj in v2.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {k2: {name: {}}}}, haved) + haved = hi + + if haved_disable is not None: + haved["disable"] = haved_disable + + 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, list_merge="append_rp") + 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 ["overridden"] and k == "disable" and not want: + want = False + if self.state in ["rendered"]: + have = None + + self.compare( + parsers=self.parsers, + want={k: want}, + have={k: have}, + ) + + self.commands = list(dict.fromkeys(self.commands)) + + def _compare_vsrvs(self, want, have): + """Compare virtual servers. + + Pre-index both want and have by (name, attribute) signature so that + each lookup is O(1) instead of O(n). Groups that are identical + between want and have are skipped entirely via an equality + short-circuit before leaf decomposition. + """ + 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", + ] + + want_index = ( + {vs["name"]: vs for vs in want.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(want, dict) + else {} + ) + have_index = ( + {vs["name"]: vs for vs in have.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(have, dict) + else {} + ) + + all_names = set(want_index) | set(have_index) + + for name in all_names: + w = want_index.get(name, {}) + h = have_index.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_named_leafs(w) if w else [] + hlist = self._extract_named_leafs(h) if h else [] + + if self.state == "rendered": + hlist = [] + + def _vsrv_sig(item): + if not isinstance(item, dict): + return None + iname = item.get("name") + if not iname: + return None + if "real_server" in item: + rs = item["real_server"] + if not isinstance(rs, dict) or "address" not in rs: + return None + addr = rs["address"] + for k in rs: + if k != "address": + return ("real_server", iname, addr, k) + return ("real_server", iname, addr, None) + for k in item: + if k != "name": + return ("attr", iname, k) + return None + + have_leaf_index = {} + for hdict in hlist: + sig = _vsrv_sig(hdict) + if sig is not None: + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = _vsrv_sig(wdict) + if sig is not None: + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + def _compare_vrrp(self, want, have): + """Compare VRRP groups and sync-groups. + + Pre-index groups by name so matching is O(1). Groups that are + identical between want and have are skipped via equality + short-circuit before any leaf decomposition occurs — this is the + dominant performance win for large idempotent runs. + """ + 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.address", + "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", + ] + + if ( + have.get("snmp") == "enabled" + and want.get("snmp") != "enabled" + and self.state not in ["deleted", "overridden"] + and (self.state != "merged" or "snmp" in want) + ): + self.commands.append("delete high-availability vrrp snmp") + + non_named = {k: v for k, v in (want or {}).items() if k not in ("groups", "sync_groups")} + non_named_have = { + k: v for k, v in (have or {}).items() if k not in ("groups", "sync_groups") + } + + hlist_non = self._extract_leaf_items(non_named_have) + wlist_non = self._extract_leaf_items(non_named) + + if self.state == "rendered": + hlist_non = [] + + have_non_index = {} + for hdict in hlist_non: + sig = self._vrrp_leaf_sig(hdict) + have_non_index[sig] = hdict + + want_non_index = {} + for wdict in wlist_non: + sig = self._vrrp_leaf_sig(wdict) + want_non_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_non_index.items(): + wdict = want_non_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_non_index.items(): + hdict = have_non_index.get(sig, {}) + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + for section in ("groups", "sync_groups"): + want_objs = (want or {}).get(section, {}) + have_objs = (have or {}).get(section, {}) + + if not isinstance(want_objs, dict): + want_objs = {} + if not isinstance(have_objs, dict): + have_objs = {} + + all_names = set(want_objs) | set(have_objs) + + for name in all_names: + w = want_objs.get(name, {}) + h = have_objs.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_leaf_items({section: {name: w}}) if w else [] + hlist = self._extract_leaf_items({section: {name: h}}) if h else [] + + if self.state == "rendered": + hlist = [] + + have_leaf_index = {} + for hdict in hlist: + sig = self._vrrp_leaf_sig(hdict) + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = self._vrrp_leaf_sig(wdict) + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + def _vrrp_leaf_sig(self, item): + """Build a hashable signature for a VRRP leaf dict for O(1) indexing.""" + if not isinstance(item, dict) or not item: + return () + + container = next(iter(item)) + inner = item[container] + + sig = [container] + + if isinstance(inner, dict) and "name" in inner: + sig.append(("name", inner["name"])) + + if isinstance(inner, dict): + for k, v in inner.items(): + if k == "name": + continue + if not isinstance(v, dict): + sig.append(k) + break + sig.append(k) + for leaf in v: + sig.append(leaf) + break + break + + return tuple(sig) + + def _list_to_named_dict(self, data): + """Convert all named-object lists to name-keyed dicts in-place. + + Replaces the three separate _vrrp_groups_list_to_dict, + _vrrp_sync_groups_list_to_dict, and _virtual_servers_list_to_dict + methods with a single helper. Also normalises real_server lists + inside virtual servers. + """ + # VRRP groups and sync_groups + vrrp = data.get("vrrp", {}) + for key in ("groups", "sync_groups"): + items = vrrp.get(key) + if isinstance(items, list): + vrrp[key] = { + item["name"]: item + for item in items + if isinstance(item, dict) and item.get("name") + } + + # Virtual servers + vss = data.get("virtual_servers") + 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 + elif isinstance(vss, dict): + for vs in vss.values(): + if not isinstance(vs, dict): + 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") + } + + 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 _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): + if all(not isinstance(i, (dict, list)) for i in v): + node[k] = sorted(v) + else: + 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 _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 + + 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 + + return [ + { + "name": parent_name, + prefix_key: data, + }, + ] + + def _prune_stubs(self, w, h, path=""): + wc = {} + hc = self._remove_defaults(h) + + if not self._remove_defaults(w) and remove_empties(hc): + self.commands = ["delete high-availability"] + return {}, {}, path + + for k, wg in (self._remove_defaults(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}", + ) + + hg.pop(name, None) + + if hg: + hc[k] = hg + else: + hc.pop(k, None) + + if self._remove_defaults(wg): + wc[k] = wg + else: + 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 + + if hi: + hc[k] = hi + else: + hc.pop(k, None) + + return wc, hc, path + + def _remove_defaults(self, data): + """Strip None and False from config dicts, but preserve "disabled". + + False is the argspec default for boolean flags (disable, no_preempt, + rfc3768_compatibility) and carries no config intent — stripping it + prevents spurious `delete` commands for fields already at their + default state. + + "disabled" is an explicit user choice for snmp and must be preserved + so that _prune_stubs can act on it. The original code stripped it, + which made `snmp: disabled` invisible to the deleted-state logic. + """ + if isinstance(data, dict): + cleaned = {} + for k, v in data.items(): + if v is None or v is False: + 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) + + def _diff_w_h(self, w, h): + + NAMED_OBJECT_KEYS = { + "groups", + "sync_groups", + "virtual_servers", + "global_parameters", + } + + if not isinstance(w, dict) or not isinstance(h, dict): + return w + + for key in w.keys() & h.keys(): + wv = w[key] + hv = h[key] + + if key in NAMED_OBJECT_KEYS and isinstance(wv, dict) and isinstance(hv, dict): + for name in wv.keys() & hv.keys(): + if wv[name] != hv[name] and isinstance(wv[name], (dict, list)): + wv[name] = {} + elif wv[name] != hv[name]: + wv[name] = None + continue + self._diff_w_h(wv, hv) + return w diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 24c444dd..7f7bad8a 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -30,6 +30,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firew from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import ( Firewall_rulesFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ha.ha import ( + HaFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.hostname.hostname import ( HostnameFacts, ) @@ -94,6 +97,7 @@ FACT_RESOURCE_SUBSETS = dict( firewall_rules=Firewall_rulesFacts, firewall_global=Firewall_globalFacts, firewall_interfaces=Firewall_interfacesFacts, + ha=HaFacts, ospfv3=Ospfv3Facts, ospfv2=Ospfv2Facts, ospf_interfaces=Ospf_interfacesFacts, diff --git a/plugins/module_utils/network/vyos/facts/ha/__init__.py b/plugins/module_utils/network/vyos/facts/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py new file mode 100644 index 00000000..fc559783 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/ha/ha.py @@ -0,0 +1,174 @@ +# -*- 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_ha fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ha.ha import ( + HaArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import ( + HaTemplate, +) + + +class HaFacts(object): + """The vyos_ha facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = HaArgs.argument_spec + + def get_config(self, connection): + return connection.get('show configuration commands | match "set high-availability"') + + def get_config_set(self, data, connection): + """Classify config lines into per-object buckets for isolated parsing. + + Each bucket is parsed by a single HaTemplate instance so that facts + from different objects (groups, sync-groups, virtual-servers) never + bleed into each other. + + Keys are namespaced to avoid collisions between a VRRP group and a + sync-group that share the same name (e.g. both named "g1"). + An elif chain ensures each line lands in exactly one bucket. + """ + config_dict = {} + for config_line in data.splitlines(): + vrrp_disable = re.search(r"set high-availability disable", config_line) + vrrp_snmp = re.search(r"set high-availability vrrp snmp", config_line) + vrrp_gp = re.search( + r"set high-availability vrrp global-parameters (\S+).*", + config_line, + ) + vrrp_grp = re.search(r"set high-availability vrrp group (\S+).*", config_line) + vrrp_sg = re.search(r"set high-availability vrrp sync-group (\S+).*", config_line) + vrrp_vsrv = re.search(r"set high-availability virtual-server (\S+).*", config_line) + + if vrrp_disable: + config_dict.setdefault("disable", []).append(config_line) + elif vrrp_snmp: + config_dict.setdefault("vrrp", []).append(config_line) + elif vrrp_gp: + config_dict.setdefault("global_parameters", []).append(config_line) + elif vrrp_grp: + key = "vrrp_group_{0}".format(vrrp_grp.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_sg: + key = "vrrp_sg_{0}".format(vrrp_sg.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_vsrv: + config_dict.setdefault(vrrp_vsrv.group(1), []).append(config_line) + + return list(config_dict.values()) + + def deep_merge(self, dest, src): + for key, value in src.items(): + if key in dest and isinstance(dest[key], dict) and isinstance(value, dict): + self.deep_merge(dest[key], value) + else: + dest[key] = value + return dest + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for vrrp network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = {} + + if not data: + data = self.get_config(connection) + resources = self.get_config_set(data, connection) + vrrp_facts = {"disable": False, "virtual_servers": {}, "vrrp": {}} + for resource in resources: + vrrp_parser = HaTemplate( + lines=resource, + module=self._module, + ) + objs = vrrp_parser.parse() + if "disable" in objs: + vrrp_facts["disable"] = objs["disable"] + + for section in ("virtual_servers", "vrrp"): + if section in objs: + for name, data in objs[section].items(): + if not isinstance(data, dict): + vrrp_facts[section][name] = data + continue + existing = vrrp_facts[section].get(name, {}) + vrrp_facts[section][name] = self.deep_merge(existing, data) + + ansible_facts["ansible_network_resources"].pop("ha", None) + + vrrp_facts = self.normalize_config(vrrp_facts) + + validate_parser = HaTemplate(lines=[], module=self._module) + params = utils.remove_empties( + validate_parser.validate_config( + self.argument_spec, + {"config": vrrp_facts}, + redact=True, + ), + ) + + facts["ha"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def normalize_config(self, config): + if not config: + return config + + if isinstance(config.get("virtual_servers"), dict): + config["virtual_servers"] = list(config["virtual_servers"].values()) + + vrrp = config.get("vrrp", {}) + + if isinstance(vrrp.get("groups"), dict): + vrrp["groups"] = list(vrrp["groups"].values()) + + if isinstance(vrrp.get("sync_groups"), dict): + vrrp["sync_groups"] = list(vrrp["sync_groups"].values()) + + # Normalize real_server inside each virtual_server + for vs in config.get("virtual_servers", []): + if isinstance(vs.get("real_server"), dict): + vs["real_server"] = list(vs["real_server"].values()) + + for group in vrrp.get("groups", []): + if isinstance(group.get("address"), list): + group["address"] = sorted(group["address"]) + + if isinstance(group.get("excluded_address"), list): + group["excluded_address"] = sorted(group["excluded_address"]) + + if isinstance(group.get("track", {}).get("interface"), list): + group["track"]["interface"] = sorted(group["track"]["interface"]) + + for sg in vrrp.get("sync_groups", []): + if isinstance(sg.get("member"), list): + sg["member"] = sorted(sg["member"]) + + return config diff --git a/plugins/module_utils/network/vyos/rm_templates/ha.py b/plugins/module_utils/network/vyos/rm_templates/ha.py new file mode 100644 index 00000000..300b14a7 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/ha.py @@ -0,0 +1,1011 @@ +# -*- 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 Ha parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_vsrvs(config_data): + config_data = config_data["virtual_servers"] + command = [] + + cmd = "high-availability virtual-server {name}".format(**config_data) + for key, value in config_data.items(): + if key == "name" or isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vsrvs_rsrv(config_data): + config_data = config_data["virtual_servers"] + command = [] + cmd = "high-availability virtual-server {name}".format(**config_data) + config_data = config_data["real_server"] + address = config_data["address"] + for key, value in config_data.items(): + if key == "address" or value is None: + continue + if value is not None and key == "health_check_script": + command.append(cmd + " real-server " + address + " health-check script " + value) + else: + command.append(cmd + " real-server " + f"{address} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_hc(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_ts(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp(config_data): + config_data = config_data["vrrp"]["global_parameters"] + command = [] + + cmd = "high-availability vrrp global-parameters".format(**config_data) + for key, value in config_data.items(): + if isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp_garp(config_data): + config_data = config_data["vrrp"]["global_parameters"]["garp"] + command = [] + cmd = "high-availability vrrp global-parameters garp" + + for key, value in config_data.items(): + if value is None: + continue + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vrrp_group(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if ( + key == "name" + or isinstance(value, dict) + or isinstance(value, list) + or isinstance(value, bool) + or value is None + ): + continue + else: + if key == "description": + value = f"'{value}'" + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(f"{cmd} {key.replace('_', '-')}") + return command + + +def _tmplt_vrrp_group_garp(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["garp"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " garp " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_auth(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["authentication"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " authentication " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_ts(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_member(config_data): + sgroup = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**sgroup) + members = sgroup.get("member", []) + for member in members: + if member is None: + continue + command.append(f"{cmd} member {member}") + return command + + +def _tmplt_vrrp_group_exaddress(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + exaddresses = group.get("excluded_address", []) + for exaddress in exaddresses: + if exaddress is None: + continue + command.append(f"{cmd} excluded-address {exaddress}") + return command + + +def _tmplt_vrrp_group_address(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + addresses = group.get("address", []) + for address in addresses: + if address is None: + continue + command.append(f"{cmd} address {address}") + return command + + +def _tmplt_vrrp_group_hc(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_track_list(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if isinstance(value, list) and value is not None and key != "name": + for item in value: + command.append(cmd + " track " + f"{key.replace('_', '-')} {item}") + return command + + +def _tmplt_vrrp_group_track_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(cmd + " track " + f"{key.replace('_', '-')}") + return command + + +class HaTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(HaTemplate, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "disable", + "getval": re.compile( + r""" + ^set + \shigh-availability + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": "high-availability disable", + "result": { + "disable": "{{ True if disable is defined else False }}", + }, + }, + { + "name": "virtual_servers.address", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+address\s+(?P<address>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "address": "{{ address if address is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.algorithm", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+algorithm\s+(?P<algorithm>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "algorithm": "{{ algorithm if algorithm is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.delay_loop", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+delay-loop\s+(?P<delay_loop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "delay_loop": "{{ delay_loop if delay_loop is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.forward_method", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+forward-method\s+(?P<forward_method>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "forward_method": "{{ forward_method if forward_method is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.fwmark", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+fwmark\s+(?P<fwmark>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "fwmark": "{{ fwmark if fwmark is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.persistence_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+persistence-timeout\s+(?P<persistence_timeout>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "persistence_timeout": "{{ persistence_timeout if persistence_timeout is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+port\s+(?P<port>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.protocol", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+protocol\s+(?P<protocol>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "protocol": "{{ protocol if protocol is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+port\s+(?P<port>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.health_check_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+health-check\sscript\s+(?P<hcscript>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "health_check_script": "{{ hcscript if hcscript is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.connection_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+connection-timeout\s+(?P<cont>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "connection_timeout": "{{ cont if cont is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.member", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \smember + \s+(?P<member>\S+) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_member, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "member": [ + "{{ member }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.health_check", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \shealth-check + (?:\s+failure-count\s+(?P<failure_count>\S+))? + (?:\s+interval\s+(?P<int>\S+))? + (?:\s+ping\s+(?P<ping>\S+))? + (?:\s+script\s+(?P<script>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_hc, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "health_check": { + "failure_count": "{{ failure_count if failure_count is defined else None }}", + "interval": "{{ int if int is defined else None }}", + "ping": "{{ ping if ping is defined else None }}", + "script": "{{ script if script is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.transition_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \stransition-script + (?:\s+backup\s+(?P<backup>\S+))? + (?:\s+fault\s+(?P<fault>\S+))? + (?:\s+master\s+(?P<master>\S+))? + (?:\s+stop\s+(?P<stop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_ts, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "transition_script": { + "backup": "{{ backup if backup is defined else None }}", + "fault": "{{ fault if fault is defined else None }}", + "master": "{{ master if master is defined else None }}", + "stop": "{{ stop if stop is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.global_parameters.garp", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sglobal-parameters + \s+garp + (?:\s+interval\s+(?P<interval>\S+))? + (?:\s+master-delay\s+(?P<master_delay>\S+))? + (?:\s+master-refresh\s+(?P<master_refresh>\S+))? + (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))? + (?:\s+master-repeat\s+(?P<master_repeat>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_gp_garp, + "result": { + "vrrp": { + "global_parameters": { + "garp": { + "interval": "{{ interval if interval is defined else None }}", + "master_delay": "{{ master_delay if master_delay is defined else None }}", + "master_refresh": "{{ master_refresh if master_refresh is defined else None }}", + "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}", + "master_repeat": "{{ master_repeat if master_repeat is defined else None }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.global_parameters", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sglobal-parameters + (?=\s+(?:startup-delay|version)\s) + (?:\s+startup-delay\s+(?P<startup_delay>\S+))? + (?:\s+version\s+(?P<version>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_gp, + "result": { + "vrrp": { + "global_parameters": { + "startup_delay": "{{ startup_delay if startup_delay is defined else None }}", + "version": "{{ version if version is defined else None }}", + }, + }, + }, + }, + { + "name": "vrrp.groups", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + (?:\s+description\s+(?P<description>'.+?'|\S+))? + (?:\s+advertise-interval\s+(?P<advertise_interval>\S+))? + (?:\s+hello-source-address\s+(?P<hello_source>\S+))? + (?:\s+interface\s+(?P<interface>\S+))? + (?:\s+peer-address\s+(?P<peer_address>\S+))? + (?:\s+preempt-delay\s+(?P<preempt_delay>\S+))? + (?:\s+priority\s+(?P<priority>\S+))? + (?:\s+vrid\s+(?P<vrid>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "description": "{{ description | replace(\"'\", \"\") if description is defined else None }}", + "advertise_interval": "{{ advertise_interval if advertise_interval is defined else None }}", + "hello_source_address": "{{ hello_source if hello_source is defined else None }}", + "interface": "{{ interface if interface is defined else None }}", + "peer_address": "{{ peer_address if peer_address is defined else None }}", + "preempt_delay": "{{ preempt_delay if preempt_delay is defined else None }}", + "priority": "{{ priority if priority is defined else None }}", + "vrid": "{{ vrid if vrid is defined else None }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.excluded_address", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \sexcluded-address + \s+(?P<excluded_address>.*) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_exaddress, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "excluded_address": [ + "{{ excluded_address | replace(\"'\", \"\") }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.address", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \saddress + \s+(?P<address>.*) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_address, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "address": [ + "{{ address | replace(\"'\", \"\") }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.garp", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \s+garp + (?:\s+interval\s+(?P<interval>\S+))? + (?:\s+master-delay\s+(?P<master_delay>\S+))? + (?:\s+master-refresh\s+(?P<master_refresh>\S+))? + (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))? + (?:\s+master-repeat\s+(?P<master_repeat>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_garp, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "garp": { + "interval": "{{ interval if interval is defined else None }}", + "master_delay": "{{ master_delay if master_delay is defined else None }}", + "master_refresh": "{{ master_refresh if master_refresh is defined else None }}", + "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}", + "master_repeat": "{{ master_repeat if master_repeat is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.authentication", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \s+authentication + (?:\s+password\s+(?P<password>\S+))? + (?:\s+type\s+(?P<type>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_auth, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "authentication": { + "password": "{{ password if password is defined else None }}", + "type": "{{ type if type is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.transition_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \stransition-script + (?:\s+backup\s+(?P<backup>\S+))? + (?:\s+fault\s+(?P<fault>\S+))? + (?:\s+master\s+(?P<master>\S+))? + (?:\s+stop\s+(?P<stop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_ts, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "transition_script": { + "backup": "{{ backup if backup is defined else None }}", + "fault": "{{ fault if fault is defined else None }}", + "master": "{{ master if master is defined else None }}", + "stop": "{{ stop if stop is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.health_check", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \shealth-check + (?:\s+failure-count\s+(?P<failure_count>\S+))? + (?:\s+interval\s+(?P<int>\S+))? + (?:\s+ping\s+(?P<ping>\S+))? + (?:\s+script\s+(?P<script>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_hc, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "health_check": { + "failure_count": "{{ failure_count if failure_count is defined else None }}", + "interval": "{{ int if int is defined else None }}", + "ping": "{{ ping if ping is defined else None }}", + "script": "{{ script if script is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.track.interface", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \strack + (?:\s+interface\s+(?P<interface>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_track_list, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "track": { + "interface": "{{ [interface.strip(\"'\")] if interface is defined else [] }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.snmp", + "getval": re.compile( + r""" + ^set + \shigh-availability + \svrrp + \s(?P<snmp>snmp) + $""", + re.VERBOSE, + ), + "setval": "high-availability vrrp snmp", + "result": { + "vrrp": { + "snmp": "{{ 'enabled' if snmp is defined else 'disabled' }}", + }, + }, + }, + { + "name": "vrrp.groups.disable", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "disable": "{{ True if disable is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.no_preempt", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<no_preempt>no-preempt) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "no_preempt": "{{ True if no_preempt is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.rfc3768_compatibility", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<rfc3768_compatibility>rfc3768-compatibility) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "rfc3768_compatibility": "{{ True if rfc3768_compatibility is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.track.exclude_vrrp_interface", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \strack + \s(?P<exclude_vrrp_inter>exclude-vrrp-interface) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_track_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "track": { + "exclude_vrrp_interface": "{{ True if exclude_vrrp_inter is defined else False }}", + }, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index 78445c02..df0a72ca 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -288,12 +288,29 @@ def in_target_not_none(h, key): def combine(a, b, recursive=False, list_merge="replace"): - """ - Merge two dictionaries (shallow or deep). - :param a: dict - :param b: dict - :param recursive: bool, deep merge - :param list_merge: str, only 'replace' is supported (default Ansible behavior) + """Merge dict ``b`` into dict ``a``, returning a new dict. + + :param a: Base dictionary. + :param b: Dictionary whose values take precedence over ``a``. + :param recursive: When True, nested dicts are merged recursively rather + than replaced wholesale. + :param list_merge: Controls how list values are combined when the same key + exists in both dicts. Supported modes: + + - ``"replace"`` *(default)* — ``b``'s list replaces ``a``'s list. + - ``"append"`` — ``b``'s list is appended to ``a``'s list (duplicates + kept). + - ``"prepend"`` — ``b``'s list is prepended to ``a``'s list + (duplicates kept). + - ``"append_rp"`` — like ``"append"`` but duplicates are removed, + preserving the first occurrence (rp = remove-preserve). + - ``"prepend_rp"`` — like ``"prepend"`` but duplicates are removed, + preserving the first occurrence. + + Passing any other value raises ``ValueError``. + :returns: New merged dict. + :raises ValueError: If either argument is not a dict, or if an + unsupported ``list_merge`` mode is given. """ if not isinstance(a, dict) or not isinstance(b, dict): raise ValueError("combine expects two dictionaries") @@ -301,8 +318,29 @@ def combine(a, b, recursive=False, list_merge="replace"): result = a.copy() for k, v in b.items(): - if recursive and k in result and isinstance(result[k], dict) and isinstance(v, dict): - result[k] = combine(result[k], v, recursive=True, list_merge=list_merge) + if k in result: + # dict merge + if recursive and isinstance(result[k], dict) and isinstance(v, dict): + result[k] = combine(result[k], v, recursive=True, list_merge=list_merge) + + # list merge + elif isinstance(result[k], list) and isinstance(v, list): + if list_merge == "replace": + result[k] = v + elif list_merge == "append": + result[k] = result[k] + v + elif list_merge == "prepend": + result[k] = v + result[k] + elif list_merge == "append_rp": + result[k] = list(dict.fromkeys(result[k] + v)) + elif list_merge == "prepend_rp": + result[k] = list(dict.fromkeys(v + result[k])) + else: + raise ValueError(f"Unsupported list_merge mode: {list_merge}") + + # everything else + else: + result[k] = v else: result[k] = v diff --git a/plugins/modules/vyos_ha.py b/plugins/modules/vyos_ha.py new file mode 100644 index 00000000..ddd02b48 --- /dev/null +++ b/plugins/modules/vyos_ha.py @@ -0,0 +1,1327 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for vyos_ha module, which manages VRRP and load balancer configuration on VyOS +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: vyos_ha +author: Evgeny Molotkov (@omnom62) +short_description: Manage VRRP and load balancer configuration on VyOS +version_added: "1.0.0" +description: + - This module configures VRRP groups, global VRRP parameters, VRRP sync groups, + and LVS-style virtual servers on VyOS 1.4+. + - Supports creation, modification, deletion, replacement, rendering, and parsing + of VRRP-related configuration. + +options: + config: + description: + - Full VRRP and virtual server configuration. + type: dict + suboptions: + disable: + description: + - Disable all VRRP and L4-LB configuration under this module. + type: bool + default: false + virtual_servers: + description: + - List of load balancer virtual server (LVS) definitions. + type: list + elements: dict + suboptions: + name: + description: + - Unique identifier for the virtual server. + type: str + required: true + address: + description: + - Virtual IP address for the server. + type: str + algorithm: + description: + - Load balancing algorithm used for dispatching connections. + type: str + delay_loop: + description: + - Delay loop interval in seconds. + type: int + forward_method: + description: + - Forwarding method used by LVS. + type: str + choices: [direct, nat] + fwmark: + description: + - Firewall mark for LVS traffic classification. + type: int + persistence_timeout: + description: + - Client persistence timeout in seconds. + type: int + port: + description: + - TCP/UDP port provided by the virtual service. + type: int + protocol: + description: + - Transport protocol for the virtual server. + type: str + choices: [tcp, udp] + + real_server: + description: + - Backend real servers behind the virtual service. + type: list + elements: dict + suboptions: + address: + description: + - Real server IP address. + type: str + required: true + port: + description: + - Backend server port. + type: int + connection_timeout: + description: + - Backend server connection timeout. + type: int + health_check_script: + description: + - Path to health check script used for backend validation. + type: str + + vrrp: + description: + - VRRP configuration including groups, global parameters, SNMP settings, + and sync-groups. + type: dict + suboptions: + + global_parameters: + description: + - Global VRRP tuning parameters. + type: dict + suboptions: + garp: + description: + - Gratuitous ARP related configuration. + type: dict + suboptions: + interval: + description: + - GARP interval in seconds. + type: int + master_delay: + description: + - Delay before sending GARP as master. + type: int + master_refresh: + description: + - Refresh interval for master GARP announcements. + type: int + master_refresh_repeat: + description: + - Number of times to repeat refresh announcements. + type: int + master_repeat: + description: + - Number of GARP repeats when transitioning to master. + type: int + + startup_delay: + description: + - Delay before VRRP starts after boot. + type: int + + version: + description: + - VRRP protocol version. + type: str + + groups: + description: + - VRRP instance configuration groups. + type: list + elements: dict + suboptions: + name: + description: + - VRRP group name. + type: str + required: true + address: + description: + - Virtual router IP addresses. + type: list + elements: str + + advertise_interval: + description: + - VRRP advertisement interval. + type: int + + authentication: + description: + - VRRP group authentication options. + type: dict + suboptions: + password: + description: + - Authentication password. + type: str + type: + description: + - Authentication type. + type: str + + description: + description: + - Text description for the VRRP group. + type: str + + disable: + description: + - Disable this VRRP group. + type: bool + default: false + + excluded_address: + description: + - IP address excluded from source checks. + type: list + elements: str + + garp: + description: + - GARP-specific settings for this group. + type: dict + suboptions: + interval: + description: GARP interval. + type: int + master_delay: + description: GARP master delay. + type: int + master_refresh: + description: GARP master refresh interval. + type: int + master_refresh_repeat: + description: Repeated refresh sends. + type: int + master_repeat: + description: GARP repeat count. + type: int + + health_check: + description: + - VRRP group health check options. + type: dict + suboptions: + failure_count: + description: Allowed number of failed checks. + type: int + interval: + description: Health check interval. + type: int + ping: + description: Host to ping for checks. + type: str + script: + description: Script to execute for health checking. + type: str + + hello_source_address: + description: + - Source address for VRRP hello packets. + type: str + + interface: + description: + - Interface used by the VRRP group. + type: str + + no_preempt: + description: + - Disable preemption. + type: bool + default: false + + peer_address: + description: + - Peer VRRP router address. + type: str + + preempt_delay: + description: + - Delay before taking master role. + type: int + + priority: + description: + - VRRP priority (higher = preferred master). + type: int + + rfc3768_compatibility: + description: + - Enable or disable RFC3768 compatibility mode. + type: bool + default: false + + track: + description: + - Track interface and VRRP behaviour. + type: dict + suboptions: + exclude_vrrp_interface: + description: + - Exclude VRRP interface from tracking. + type: bool + interface: + description: + - Interface to track. + type: list + elements: str + + transition_script: + description: + - Scripts executed during VRRP state transitions. + type: dict + suboptions: + backup: + description: Path to backup script. + type: str + fault: + description: Path to fault script. + type: str + master: + description: Path to master script. + type: str + stop: + description: Path to stop script. + type: str + + vrid: + description: + - VRRP Virtual Router ID. + type: int + + snmp: + description: + - Enable SNMP support for VRRP. + type: str + choices: ['enabled', 'disabled'] + + sync_groups: + description: + - VRRP sync-groups for coordinated failover. + type: list + elements: dict + suboptions: + name: + description: + - Sync-group name. + type: str + required: true + + health_check: + description: + - Health check options for sync group. + type: dict + suboptions: + failure_count: + description: Allowed number of failures. + type: int + interval: + description: Health check interval. + type: int + ping: + description: Host to ping. + type: str + script: + description: Script to run for health checking. + type: str + + member: + description: + - List of VRRP groups participating in this sync group. + type: list + elements: str + + transition_script: + description: + - Transition scripts for sync group events. + type: dict + suboptions: + backup: + description: Backup state script. + type: str + fault: + description: Fault state script. + type: str + master: + description: Master state script. + type: str + stop: + description: Stop state script. + type: str + + state: + description: + - Desired end state of the VRRP configuration. + type: str + choices: + - deleted + - merged + - purged + - replaced + - gathered + - rendered + - parsed + - overridden + default: merged + + running_config: + description: + - Used only when C(state=parsed). Must contain the output of + C(show configuration commands | grep high-availability). + type: str +""" + +EXAMPLES = """ +# Using merged +# Before state + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# vyos@vyos:~$ + +- name: Merge provided configuration with device configuration + vyos.vyos.vyos_ha: + config: + disable: true + virtual_servers: + - name: s1 + address: 10.10.10.5 + algorithm: round-robin + real_server: + - address: 10.10.50.2 + port: 8443 + - name: s2 + address: 10.10.10.2 + persistence_timeout: 30 + port: 81 + protocol: tcp + - name: s3 + address: 10.10.10.3 + port: 88 + protocol: udp + vrrp: + snmp: enabled + global_parameters: + startup_delay: 30 + garp: + master_repeat: 6 + groups: + - name: "g1" + peer_address: 192.168.1.3 + priority: 100 + disable: false + no_preempt: false + vrid: 20 + sync_groups: + - name: "sg1" + health_check: + failure_count: 5 + state: merged + +# After State +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s1 address '10.10.10.5' +# set high-availability virtual-server s1 algorithm 'round-robin' +# set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 '30' +# 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' +# vyos@vyos:~$ +# +# # Module Execution: +# +# "after": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.5", +# "algorithm": "round-robin", +# "name": "s1", +# "real_server": [ +# { +# "address": "10.10.50.2", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# }, +# "startup_delay": 30 +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "before": { +# "disable": false +# }, +# "changed": true, +# "commands": [ +# "set high-availability disable", +# "set high-availability virtual-server s1 address 10.10.10.5", +# "set high-availability virtual-server s1 algorithm round-robin", +# "set high-availability virtual-server s1 real-server 10.10.50.2 port 8443", +# "set high-availability virtual-server s2 address 10.10.10.2", +# "set high-availability virtual-server s2 persistence-timeout 30", +# "set high-availability virtual-server s2 port 81", +# "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 30", +# "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 snmp", +# "set high-availability vrrp sync-group sg1 health-check failure-count 5" +# ], + +# Using replaced: +# -------------- + +# Before state: +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s1 address '10.10.10.5' +# set high-availability virtual-server s1 algorithm 'round-robin' +# set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 '30' +# 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' +# vyos@vyos:~$ + +- name: Replace + vyos.vyos.vyos_ha: + config: + disable: false + virtual_servers: + - name: s1 + address: 10.10.10.3 + algorithm: round-robin + port: 8443 + real_server: + - address: 10.10.50.3 + port: 8443 + - name: s2 + address: 10.10.10.2 + persistence_timeout: 300 + port: 81 + protocol: tcp + real_server: + - address: 10.10.50.30 + port: 8443 + - name: s3 + address: 10.10.10.3 + port: 88 + protocol: udp + real_server: + - address: 10.10.50.6 + port: 8443 + vrrp: + snmp: enabled + global_parameters: + startup_delay: 30 + garp: + master_repeat: 6 + groups: + - name: "g1" + peer_address: 192.168.1.13 + priority: 100 + disable: false + no_preempt: true + interface: eth1 + address: 192.168.51.13 + vrid: 20 + sync_groups: + - name: "sg1" + health_check: + failure_count: 3 + state: replaced + +# After state: + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability virtual-server s1 address '10.10.10.3' +# set high-availability virtual-server s1 algorithm 'round-robin' +# set high-availability virtual-server s1 port '8443' +# set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' +# set high-availability virtual-server s1 real-server 10.10.50.3 port '8443' +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '300' +# set high-availability virtual-server s2 port '81' +# set high-availability virtual-server s2 protocol 'tcp' +# set high-availability virtual-server s2 real-server 10.10.50.3 port '8443' +# 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 virtual-server s3 real-server 10.10.50.6 port '8443' +# set high-availability vrrp global-parameters garp master-repeat '6' +# set high-availability vrrp global-parameters startup-delay '30' +# set high-availability vrrp group g1 address 192.168.51.13 +# set high-availability vrrp group g1 interface 'eth1' +# 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 peer-address '192.168.1.13' +# set high-availability vrrp group g1 priority '100' +# 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' +# vyos@vyos:~$ +# +# +# Module Execution: +# +# "after": { +# "disable": false, +# "virtual_servers": [ +# { +# "address": "10.10.10.3", +# "algorithm": "round-robin", +# "name": "s1", +# "port": 8443, +# "real_server": [ +# { +# "address": "10.10.50.2", +# "port": 8443 +# }, +# { +# "address": "10.10.50.3", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 300, +# "port": 81, +# "protocol": "tcp", +# "real_server": [ +# { +# "address": "10.10.50.3", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp", +# "real_server": [ +# { +# "address": "10.10.50.6", +# "port": 8443 +# } +# ] +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# }, +# "startup_delay": 30 +# }, +# "groups": [ +# { +# "address": "192.168.51.13", +# "disable": false, +# "interface": "eth1", +# "name": "g1", +# "no_preempt": true, +# "peer_address": "192.168.1.13", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 3 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "before": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.5", +# "algorithm": "round-robin", +# "name": "s1", +# "real_server": [ +# { +# "address": "10.10.50.2", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# }, +# "startup_delay": 30 +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "changed": true, +# "commands": [ +# "delete high-availability disable", +# "set high-availability virtual-server s1 address 10.10.10.3", +# "set high-availability virtual-server s1 port 8443", +# "set high-availability virtual-server s1 real-server 10.10.50.3 port 8443", +# "set high-availability virtual-server s2 persistence-timeout 300", +# "set high-availability virtual-server s2 real-server 10.10.50.3 port 8443", +# "set high-availability virtual-server s3 real-server 10.10.50.6 port 8443", +# "set high-availability vrrp group g1 address 192.168.51.13", +# "set high-availability vrrp group g1 interface eth1", +# "set high-availability vrrp group g1 no-preempt", +# "set high-availability vrrp group g1 peer-address 192.168.1.13", +# "set high-availability vrrp sync-group sg1 health-check failure-count 3" +# ], + +# Using deleted: +# ------------- + +# Before state: + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s1 address '10.10.10.5' +# set high-availability virtual-server s1 algorithm 'round-robin' +# set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 '30' +# 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' +# vyos@vyos:~$ + +- name: Delete configuration + vyos.vyos.vyos_ha: + config: + disable: false + vrrp: + snmp: disabled + global_parameters: + startup_delay: 32 + version: 3 + virtual_servers: + - name: 's1' + address: '10.10.10.1' + algorithm: 'round-robin' + delay_loop: 60 + forward_method: 'direct' + persistence_timeout: 30 + port: 443 + protocol: 'tcp' + real_server: + - address: '10.10.10.1' + connection_timeout: 61 + port: 443 + state: deleted + +# After state: + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' + +# vyos@vyos:~$ +# +# +# Module Execution: +# +# "after": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# } +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "before": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.5", +# "algorithm": "round-robin", +# "name": "s1", +# "real_server": [ +# { +# "address": "10.10.50.2", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# }, +# "startup_delay": 30 +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "changed": true, +# "commands": [ +# "delete high-availability virtual-server s1", +# "delete high-availability vrrp global-parameters startup-delay" +# ], + +# Using purged: + +# Before state: + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' +# vyos@vyos:~$ + + +- name: Purge configuration + vyos.vyos.vyos_ha: + config: + state: purged + +# After state: + +# vyos@vyos:~$ show configuration commands | match "set high-availability" +# vyos@vyos:~$ +# +# Module Execution: +# +# "after": { +# "disable": false +# }, +# "before": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# } +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# "changed": true, +# "commands": [ +# "delete high-availability" +# ], + + +# using gathered: +# -------------- + +# Before state: +# vyos@vyos:~$ +# show configuration commands | match "set high-availability" +# set high-availability disable +# set high-availability virtual-server s1 address '10.10.10.5' +# set high-availability virtual-server s1 algorithm 'round-robin' +# set high-availability virtual-server s1 real-server 10.10.50.2 port '8443' +# set high-availability virtual-server s2 address '10.10.10.2' +# set high-availability virtual-server s2 persistence-timeout '30' +# set high-availability virtual-server s2 port '81' +# 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 '30' +# 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 snmp +# set high-availability vrrp sync-group sg1 health-check failure-count '5' +# vyos@vyos:~$ + +- name: gather configs + vyos.vyos.vyos_ha: + state: gathered + +# Module Execution: +# "changed": false, +# "gathered": { +# "disable": true, +# "virtual_servers": [ +# { +# "address": "10.10.10.5", +# "algorithm": "round-robin", +# "name": "s1", +# "real_server": [ +# { +# "address": "10.10.50.2", +# "port": 8443 +# } +# ] +# }, +# { +# "address": "10.10.10.2", +# "name": "s2", +# "persistence_timeout": 30, +# "port": 81, +# "protocol": "tcp" +# }, +# { +# "address": "10.10.10.3", +# "name": "s3", +# "port": 88, +# "protocol": "udp" +# } +# ], +# "vrrp": { +# "global_parameters": { +# "garp": { +# "master_repeat": 6 +# }, +# "startup_delay": 30 +# }, +# "groups": [ +# { +# "disable": false, +# "name": "g1", +# "no_preempt": false, +# "peer_address": "192.168.1.3", +# "priority": 100, +# "rfc3768_compatibility": false, +# "vrid": 20 +# } +# ], +# "snmp": "enabled", +# "sync_groups": [ +# { +# "health_check": { +# "failure_count": 5 +# }, +# "name": "sg1" +# } +# ] +# } +# }, +# + +# Using parsed: +# ------------ + +# parsed.cfg +# set high-availability vrrp group g1 interface eth2 +# set high-availability vrrp group g1 address 1.1.1.1 +# set high-availability vrrp group g1 disable +# set high-availability vrrp group g1 no-preempt +# set high-availability vrrp group g1 advertise-interval 10 +# set high-availability vrrp group g1 peer-address 2.2.2.2 +# set high-availability vrrp group g1 rfc3768-compatibility +# set high-availability vrrp group g1 vrid 20 + +- name: parse configs + vyos.vyos.vyos_ha: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Module execution: +# "parsed": { +# "disable": false, +# "vrrp": { +# "groups": [ +# { +# "address": "1.1.1.1", +# "advertise_interval": 10, +# "disable": true, +# "interface": "eth2", +# "name": "g1", +# "no_preempt": true, +# "peer_address": "2.2.2.2", +# "rfc3768_compatibility": true, +# "vrid": 20 +# } +# ] +# } +# } +# + +# Using rendered: +# -------------- + +- name: Render + vyos.vyos.vyos_ha: + config: + disable: true + vrrp: + snmp: enabled + global_parameters: + startup_delay: 32 + version: 3 + garp: + interval: 30 + master_delay: 11 + master_refresh: 100 + master_refresh_repeat: 200 + master_repeat: 5 + state: rendered + +# Module Execution: +# "rendered": [ +# "set high-availability disable", +# "set high-availability vrrp global-parameters garp interval 30", +# "set high-availability vrrp global-parameters garp master-delay 11", +# "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 32", +# "set high-availability vrrp global-parameters version 3", +# "set high-availability vrrp snmp" +# ] +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - 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 description 'Group 1' +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - 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' +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ha.ha import ( + HaArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ha.ha import ( + Ha, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=HaArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Ha(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() |
