diff --git a/plugins/module_utils/network/vyos/config/ha/ha.py b/plugins/module_utils/network/vyos/config/ha/ha.py index db1faae0..7fc96a35 100644 --- a/plugins/module_utils/network/vyos/config/ha/ha.py +++ b/plugins/module_utils/network/vyos/config/ha/ha.py @@ -1,722 +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", ] - # Validate once at construction time rather than on every parse() - # and get_parser() call. get_os_version() triggers a device - # round-trip; calling it 20+ times per module execution is wasteful. + 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}, ) - # Deduplicate while preserving insertion order. Do NOT sort — - # sorting breaks set/delete ordering and makes the command list - # harder to reason about. 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", ] - # Build name-keyed indexes once — O(n) — rather than scanning the - # full list for every item — O(n²). 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, {}) - # Short-circuit: identical virtual servers need no commands. 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 = [] - # Build leaf-level indexes for this server. 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") - # Process global_parameters and snmp via the existing leaf extractor - # since they are not named objects. Only groups/sync_groups get the - # indexed treatment. 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 = [] - # Build leaf index for non-named items. 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}) - # Process named objects (groups, sync_groups) with per-name - # short-circuit and indexed leaf lookup. 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, {}) - # Short-circuit: identical objects need no commands. 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 = [] - # Index leaves for this named object. 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/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py index 26a1f34e..fc559783 100644 --- a/plugins/module_utils/network/vyos/facts/ha/ha.py +++ b/plugins/module_utils/network/vyos/facts/ha/ha.py @@ -1,183 +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: - # Namespace with prefix to avoid collision with sync-groups - # that share the same name. 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) - # normalize_config must run before validate_config so that - # virtual_servers, groups, and sync_groups are already lists. - # validate_config cannot coerce a keyed dict to a list and will - # fail_json if it receives one. 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 - # Normalize virtual_servers dict → list. This conversion is safe to - # call repeatedly (already-a-list case is a no-op) but should only - # be needed once — after validate_config has run. 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 index 870ec5cf..300b14a7 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ha.py +++ b/plugins/module_utils/network/vyos/rm_templates/ha.py @@ -1,1024 +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(?Pdisable) $""", 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\S+) (?:\s+address\s+(?P
\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\S+) (?:\s+algorithm\s+(?P\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\S+) (?:\s+delay-loop\s+(?P\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\S+) (?:\s+forward-method\s+(?P\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\S+) (?:\s+fwmark\s+(?P\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\S+) (?:\s+persistence-timeout\s+(?P\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\S+) (?:\s+port\s+(?P\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\S+) (?:\s+protocol\s+(?P\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\S+) \sreal-server \s+(?P
\S+) (?:\s+port\s+(?P\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\S+) \sreal-server \s+(?P
\S+) (?:\s+health-check\sscript\s+(?P\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\S+) \sreal-server \s+(?P
\S+) (?:\s+connection-timeout\s+(?P\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\S+) \smember \s+(?P\S+) $ """, re.VERBOSE, ), "setval": _tmplt_vrrp_sgroup_member, "result": { "vrrp": { "sync_groups": { "{{ sgname }}": { "name": "{{ sgname }}", "member": [ "{{ member }}", ], }, }, }, }, }, - # vrrp.sync_groups.health_check — all sub-fields are independently - # optional. VyOS emits one field per line; placing ? inside each - # group (not on a separate line) ensures every single-field line - # matches regardless of which field is present. { "name": "vrrp.sync_groups.health_check", "getval": re.compile( r""" ^set\shigh-availability\svrrp\ssync-group \s+(?P\S+) \shealth-check (?:\s+failure-count\s+(?P\S+))? (?:\s+interval\s+(?P\S+))? (?:\s+ping\s+(?P\S+))? (?:\s+script\s+(?P