diff options
Diffstat (limited to 'plugins')
160 files changed, 19273 insertions, 1924 deletions
diff --git a/plugins/action/file.py b/plugins/action/file.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/file.py @@ -0,0 +1 @@ +vyos.py
\ No newline at end of file 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/action/nat.py b/plugins/action/nat.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/nat.py @@ -0,0 +1 @@ +vyos.py
\ No newline at end of file diff --git a/plugins/action/vrf.py b/plugins/action/vrf.py new file mode 120000 index 00000000..331a791f --- /dev/null +++ b/plugins/action/vrf.py @@ -0,0 +1 @@ +vyos.py
\ No newline at end of file diff --git a/plugins/action/vyos.py b/plugins/action/vyos.py index 148d7c64..df8e7127 100644 --- a/plugins/action/vyos.py +++ b/plugins/action/vyos.py @@ -18,7 +18,6 @@ # from __future__ import absolute_import, division, print_function - __metaclass__ = type from ansible.utils.display import Display @@ -26,7 +25,6 @@ from ansible_collections.ansible.netcommon.plugins.action.network import ( ActionModule as ActionNetworkModule, ) - display = Display() diff --git a/plugins/cliconf/vyos.py b/plugins/cliconf/vyos.py index 5beffaa1..e693f820 100644 --- a/plugins/cliconf/vyos.py +++ b/plugins/cliconf/vyos.py @@ -49,11 +49,13 @@ import re from ansible.errors import AnsibleConnectionFailure from ansible.module_utils._text import to_text from ansible.module_utils.common._collections_compat import Mapping +from ansible.plugins.cliconf import CliconfBase from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( NetworkConfig, ) from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list -from ansible_collections.ansible.netcommon.plugins.plugin_utils.cliconf_base import CliconfBase + +from ansible_collections.vyos.vyos.plugins.cliconf_utils.vyosconf import VyosConf class Cliconf(CliconfBase): @@ -122,7 +124,15 @@ class Cliconf(CliconfBase): out = self.send_command(command) return out - def edit_config(self, candidate=None, commit=True, replace=None, comment=None): + def edit_config( + self, + candidate=None, + commit=True, + replace=None, + diff=False, + comment=None, + confirm=None, + ): resp = {} operations = self.get_device_operations() self.check_edit_config_capability(operations, candidate, commit, replace, comment) @@ -143,7 +153,7 @@ class Cliconf(CliconfBase): if diff_config: if commit: try: - self.commit(comment) + self.commit(comment, confirm) except AnsibleConnectionFailure as e: msg = "commit failed: %s" % e.message self.discard_changes() @@ -191,12 +201,19 @@ class Cliconf(CliconfBase): check_all=check_all, ) - def commit(self, comment=None): - if comment: - command = 'commit comment "{0}"'.format(comment) + def commit(self, comment=None, confirm=None): + if confirm: + if comment: + command = 'commit-confirm {0} comment "{1}"'.format(confirm, comment) + else: + command = "commit-confirm {0}".format(confirm) + self.send_command(command, "Proceed?", "\n") else: - command = "commit" - self.send_command(command) + if comment: + command = 'commit comment "{0}"'.format(comment) + else: + command = "commit" + self.send_command(command) def discard_changes(self): self.send_command("exit discard") @@ -232,12 +249,21 @@ class Cliconf(CliconfBase): if path: raise ValueError("'path' in diff is not supported") - set_format = candidate.startswith("set") or candidate.startswith("delete") + first_line = next( + ( + stripped + for stripped in (line.strip() for line in candidate.splitlines()) + if stripped and not stripped.startswith("#") + ), + "", + ) + set_format = first_line.startswith("set") or first_line.startswith("delete") candidate_obj = NetworkConfig(indent=4, contents=candidate) + if not set_format: + config = [c.line for c in candidate_obj.items] commands = list() - # this filters out less specific lines for item in config: for index, entry in enumerate(commands): if item.startswith(entry): @@ -248,11 +274,66 @@ class Cliconf(CliconfBase): candidate_commands = ["set %s" % cmd.replace(" {", "") for cmd in commands] else: - candidate_commands = str(candidate).strip().split("\n") + + candidate_commands = [ + line.strip() + for line in str(candidate).splitlines() + if line.strip() and not line.lstrip().startswith("#") + ] if diff_match == "none": diff["config_diff"] = list(candidate_commands) return diff + if diff_match == "enforce": + if running is None: + raise ValueError( + "diff_match=enforce requires a running configuration to diff against", + ) + + enforce_candidate_lines = list(candidate_commands) + + if not enforce_candidate_lines: + raise ValueError( + "diff_match=enforce received an empty candidate (after stripping blank/" + "comment lines); refusing to treat that as a desired end-state of " + "'delete everything'. Provide 'set' commands describing the desired " + "configuration.", + ) + + for line in enforce_candidate_lines: + tokens = line.strip().split() + if tokens[0] != "set": + raise ValueError( + "diff_match=enforce treats the candidate as the complete desired " + "configuration end-state and only supports 'set' commands; " + "line does not start with 'set' (found: {0!r})".format( + line.strip(), + ), + ) + if len(VyosConf().parse_line(line)[1]) < 1: + raise ValueError( + "diff_match=enforce only supports complete 'set' commands with at least " + "a path and a leaf; got: {0!r}".format(line.strip()), + ) + running_conf = VyosConf( + [ + line + for line in running.splitlines() + if line.strip() and not line.lstrip().startswith("#") + ], + ) + + candidate_conf = VyosConf(enforce_candidate_lines) + diff["config_diff"] = running_conf.diff_commands_to(candidate_conf) + for cmd in diff["config_diff"]: + if re.match(r"^delete\s+service\s+ssh\b", cmd): + raise ValueError( + "diff_match=enforce refuses to generate 'delete service ssh ...' " + "commands, since this could sever the management connection. " + "Remove SSH configuration explicitly with a separate match=line " + "or match=none task instead.", + ) + return diff running_commands = [str(c).replace("'", "") for c in running.splitlines()] @@ -269,12 +350,14 @@ class Cliconf(CliconfBase): updates.append(line) elif item.startswith("delete"): + if not running_commands: updates.append(line) else: item = re.sub(r"delete", "set", item) + for entry in running_commands: - if entry.startswith(item) and line not in visited: + if re.match(rf"^{re.escape(item)}\b", entry) and line not in visited: updates.append(line) visited.add(line) @@ -323,7 +406,7 @@ class Cliconf(CliconfBase): def get_option_values(self): return { "format": ["text", "set"], - "diff_match": ["line", "none"], + "diff_match": ["line", "enforce", "none"], "diff_replace": [], "output": [], } diff --git a/plugins/cliconf_utils/__init__.py b/plugins/cliconf_utils/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/cliconf_utils/__init__.py diff --git a/plugins/cliconf_utils/vyosconf.py b/plugins/cliconf_utils/vyosconf.py new file mode 100644 index 00000000..a3ed6887 --- /dev/null +++ b/plugins/cliconf_utils/vyosconf.py @@ -0,0 +1,257 @@ +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + + +class _KeepExistingSentinel: + """Unique marker for 'preserve whatever's already here' in a diff. + Deliberately not a plain string/value: a real config leaf could + legitimately be "..." (e.g. a description), and a string sentinel + would collide with it. An object identity never can. + """ + + def __repr__(self): + return "<keep-existing>" + + +KEEP_EXISTING_VALUES = _KeepExistingSentinel() + + +class VyosConf: + def __init__(self, commands=None): + self.config = {} + if isinstance(commands, list): + self.run_commands(commands) + + def set_entry(self, path, leaf): + """ + This function sets a value in the configuration given a path. + :param path: list of strings to traverse in the config + :param leaf: value to set at the destination + :return: dict + """ + target = self.config + path = path + [leaf] + for key in path: + if key not in target or not isinstance(target[key], dict): + target[key] = {} + target = target[key] + return self.config + + def del_entry(self, path, leaf): + """ + This function deletes a value from the configuration given a path + and also removes all the parents that are now empty. If the leaf + does not exist at the given path, the configuration is left + unchanged (delete is treated as a no-op, matching VyOS's own + behaviour when deleting a path that isn't set). + :param path: list of strings to traverse in the config + :param leaf: value to delete at the destination + :return: dict + """ + target = self.config + first_no_sibling_key = None + for key in path: + if key not in target: + return self.config + if len(target[key]) <= 1: + if first_no_sibling_key is None: + first_no_sibling_key = [target, key] + else: + first_no_sibling_key = None + target = target[key] + + if leaf not in target: + return self.config + + if first_no_sibling_key is None: + first_no_sibling_key = [target, leaf] + + target = first_no_sibling_key[0] + target_key = first_no_sibling_key[1] + del target[target_key] + return self.config + + def check_entry(self, path, leaf): + """ + This function checks if a value exists in the config. + :param path: list of strings to traverse in the config + :param leaf: value to check for existence + :return: bool + """ + target = self.config + path = path + [leaf] + for key in path: + if key not in target or not isinstance(target[key], dict): + return False + target = target[key] + return True + + def parse_line(self, line): + """ + This function parses a given command from string. + :param line: line to parse + :return: [command, path, leaf] + """ + line = re.match(r"^('(.*)'|\"(.*)\"|([^#\"']*))*", line).group(0).strip() + if not line: + return ["", [], ""] + path = re.findall(r"('.*?'|\".*?\"|\S+)", line) + if not path: + return ["", [], ""] + leaf = path[-1] + if leaf.startswith('"') and leaf.endswith('"'): + leaf = leaf[1:-1] + if leaf.startswith("'") and leaf.endswith("'"): + leaf = leaf[1:-1] + return [path[0], path[1:-1], leaf] + + def run_command(self, command): + """ + This function runs a given command string. + :param command: command to run + :return: dict + """ + [cmd, path, leaf] = self.parse_line(command) + if cmd.startswith("set"): + self.set_entry(path, leaf) + if cmd.startswith("del"): + self.del_entry(path, leaf) + return self.config + + def run_commands(self, commands): + """ + This function runs a list of command strings. + :param commands: commands to run + :return: dict + """ + for c in commands: + self.run_command(c) + return self.config + + def check_command(self, command): + """ + This function checks a command for existence in the config. + :param command: command to check + :return: bool + """ + [cmd, path, leaf] = self.parse_line(command) + if cmd.startswith("set"): + return self.check_entry(path, leaf) + if cmd.startswith("del"): + return not self.check_entry(path, leaf) + return True + + def check_commands(self, commands): + """ + This function checks a list of commands for existence in the config. + :param commands: list of commands to check + :return: [bool] + """ + return [self.check_command(c) for c in commands] + + def quote_key(self, key): + """ + This function adds quotes to key if quotes are needed for correct parsing. + :param key: str to wrap in quotes if needed + :return: str + """ + if len(key) == 0: + return "" + if '"' in key: + return "'" + key + "'" + if "'" in key: + return '"' + key + '"' + if not re.match(r"^[a-zA-Z0-9./-]*$", key): + return "'" + key + "'" + return key + + def build_commands(self, structure=None, nested=False): + """ + This function builds a list of commands to recreate the current configuration. + :return: [str] + """ + if not isinstance(structure, dict): + structure = self.config + if len(structure) == 0: + return [""] if nested else [] + commands = [] + for key, value in structure.items(): + quoted_key = self.quote_key(key) + for c in self.build_commands(value, True): + commands.append((quoted_key + " " + c).strip()) + if nested: + return commands + return ["set " + c for c in commands] + + def diff_to(self, other, structure): + if not isinstance(other, dict): + other = {} + if len(structure) == 0: + return ([], [""]) + if not isinstance(structure, dict): + structure = {} + if len(other) == 0: + return ([""], []) + if len(other) == 0 and len(structure) == 0: + return ([], []) + + toset = [] + todel = [] + for key in structure.keys(): + quoted_key = self.quote_key(key) + if key in other: + # keys in both configs, pls compare subkeys + (subset, subdel) = self.diff_to(other[key], structure[key]) + for s in subset: + toset.append(quoted_key + " " + s) + for d in subdel: + todel.append(quoted_key + " " + d) + else: + # keys only in this, delete if KEEP_EXISTING_VALUES not set + if KEEP_EXISTING_VALUES not in other: + todel.append(quoted_key) + continue # del + for key, value in other.items(): + if key == KEEP_EXISTING_VALUES: + continue + quoted_key = self.quote_key(key) + if key not in structure: + # keys only in other, pls set all subkeys + (subset, subdel) = self.diff_to(other[key], None) + for s in subset: + toset.append(quoted_key + " " + s) + + return (toset, todel) + + def diff_commands_to(self, other): + """ + This function calculates the required commands to change the current into + the given configuration. Only top-level sections present in the desired + configuration are enforced; top-level sections the candidate does not + mention at all are left completely untouched. + :param other: VyosConf + :return: [str] + """ + scoped_structure = {k: v for k, v in self.config.items() if k in other.config} + (toset, todel) = self.diff_to(other.config, scoped_structure) + return ["delete " + c.strip() for c in todel] + ["set " + c.strip() for c in toset] diff --git a/plugins/doc_fragments/vyos.py b/plugins/doc_fragments/vyos.py index aaa7bf71..698d0f5f 100644 --- a/plugins/doc_fragments/vyos.py +++ b/plugins/doc_fragments/vyos.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function - __metaclass__ = type # Copyright: (c) 2015, Peter Sprygada <psprygada@ansible.com> diff --git a/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py index 13f9fab0..946f6861 100644 --- a/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py index 42fb5abf..13cca54d 100644 --- a/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/facts/facts.py b/plugins/module_utils/network/vyos/argspec/facts/facts.py index b274c507..ddcd8d14 100644 --- a/plugins/module_utils/network/vyos/argspec/facts/facts.py +++ b/plugins/module_utils/network/vyos/argspec/facts/facts.py @@ -4,8 +4,8 @@ """ The arg spec for the vyos facts module. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py index 8421b6de..dd17ef85 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py @@ -4,31 +4,12 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -############################################# -# WARNING # -############################################# -# -# This file is auto generated by the resource -# module builder playbook. -# -# Do not edit this file manually. -# -# Changes to this file will be over written -# by the resource module builder. -# -# Changes should be made in the model used to -# generate this file or in the resource module -# builder template. -# -############################################# """ The arg spec for the vyos_firewall_global module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -187,6 +168,64 @@ class Firewall_globalArgs(object): # pylint: disable=R0903 ], "type": "str", }, + "zone": { + "elements": "dict", + "type": "list", + "options": { + "description": {"type": "str"}, + "default_action": { + "choices": [ + "drop", + "reject", + ], + "default": "drop", + "type": "str", + }, + "default_log": {"type": "bool"}, + "interfaces": { + "elements": "str", + "type": "list", + }, + "local_zone": {"type": "bool"}, + "name": { + "required": True, + "type": "str", + }, + "intra_zone_filtering": { + "type": "dict", + "options": { + "action": { + "choices": ["accept", "drop"], + "type": "str", + }, + "firewall": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "ipv6_name": {"type": "str"}, + }, + }, + }, + }, + "sources": { + "elements": "dict", + "type": "list", + "options": { + "zone": { + "required": True, + "type": "str", + }, + "firewall": { + "type": "dict", + "options": { + "name": {"type": "str"}, + "ipv6_name": {"type": "str"}, + }, + }, + }, + }, + }, + }, }, "type": "dict", }, diff --git a/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py index 93c898e8..d925a7f4 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_firewall_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py index 6ae17585..0378a65b 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py @@ -25,10 +25,8 @@ The arg spec for the vyos_firewall_rules module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -56,6 +54,8 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "reject", "accept", "jump", + "return", + "continue", ], "type": "str", }, @@ -83,6 +83,7 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "continue", "return", "jump", + "offload", "queue", "synproxy", ], @@ -210,6 +211,7 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "required": True, "type": "int", }, + "offload_target": {"type": "str"}, "outbound_interface": { "options": { "group": {"type": "str"}, 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/argspec/hostname/hostname.py b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py index 12864e66..b1af7b9c 100644 --- a/plugins/module_utils/network/vyos/argspec/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py index 14b67c28..8ba119f3 100644 --- a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py @@ -4,31 +4,12 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -############################################# -# WARNING # -############################################# -# -# This file is auto generated by the resource -# module builder playbook. -# -# Do not edit this file manually. -# -# Changes to this file will be over written -# by the resource module builder. -# -# Changes should be made in the model used to -# generate this file or in the resource module -# builder template. -# -############################################# """ The arg spec for the vyos_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -79,6 +60,7 @@ class InterfacesArgs(object): # pylint: disable=R0903 }, "type": "list", }, + "vrf": {"type": "str"}, }, "type": "list", }, diff --git a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py index 4dee518e..d6d9e298 100644 --- a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_l3_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py index 956c4385..7744eea0 100644 --- a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lag_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py index d79de617..6d1129e8 100644 --- a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lldp_global module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py index fd6c6271..4bb742f5 100644 --- a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lldp_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py index 734d190e..f6800e9b 100644 --- a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/nat/__init__.py b/plugins/module_utils/network/vyos/argspec/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py new file mode 100644 index 00000000..6f81c53c --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- +# 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 arg spec for the vyos_nat module +""" + + +class NatArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_nat module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "nat": { + "type": "dict", + "options": { + "cgnat": { + "type": "dict", + "options": { + "log_allocation": { + "type": "bool", + }, + "pool": { + "type": "dict", + "options": { + "external": { + "type": "list", + "elements": "dict", + "options": { + "name": { + "type": "str", + "required": True, + }, + "external_port_range": { + "type": "str", + }, + "per_user_limit": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + }, + }, + "range": { + "type": "list", + "elements": "dict", + "options": { + "value": { + "type": "str", + "required": True, + }, + "seq": { + "type": "str", + }, + }, + }, + }, + }, + "internal": { + "type": "list", + "elements": "dict", + "options": { + "name": { + "type": "str", + "required": True, + }, + "range": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "source": { + "type": "dict", + "options": { + "pool": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "pool": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "protocol": { + "type": "str", + }, + "packet_type": { + "type": "str", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "disable": { + "type": "bool", + }, + "inbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + "group": { + "type": "str", + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "fqdn": { + "type": "str", + }, + "port": { + "type": "str", + }, + "address_group": { + "type": "str", + }, + "domain_group": { + "type": "str", + }, + "mac_group": { + "type": "str", + }, + "network_group": { + "type": "str", + }, + "port_group": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + "redirect_port": { + "type": "str", + }, + "address_mapping": { + "type": "str", + "choices": [ + "random", + "persistent", + ], + }, + "port_mapping": { + "type": "str", + "choices": [ + "random", + "none", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "protocol": { + "type": "str", + }, + "packet_type": { + "type": "str", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "disable": { + "type": "bool", + }, + "outbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + "group": { + "type": "str", + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "fqdn": { + "type": "str", + }, + "address_group": { + "type": "str", + }, + "domain_group": { + "type": "str", + }, + "mac_group": { + "type": "str", + }, + "network_group": { + "type": "str", + }, + "port_group": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "source": { + "type": "dict", + "options": { + "address": {"type": "str"}, + "fqdn": {"type": "str"}, + "port": {"type": "str"}, + "address_group": {"type": "str"}, + "domain_group": {"type": "str"}, + "mac_group": {"type": "str"}, + "network_group": {"type": "str"}, + "port_group": {"type": "str"}, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + "address_mapping": { + "type": "str", + "choices": [ + "random", + "persistent", + ], + }, + "port_mapping": { + "type": "str", + "choices": [ + "random", + "none", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "static": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + }, + }, + "inbound_interface": { + "type": "str", + }, + "log": { + "type": "bool", + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "nat64": { + "type": "dict", + "options": { + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "disable": { + "type": "bool", + }, + "match": { + "type": "dict", + "options": { + "mark": { + "type": "int", + }, + }, + }, + "source": { + "type": "dict", + "options": { + "prefix": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "pool": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "address": { + "type": "str", + }, + "description": { + "type": "str", + }, + "disable": { + "type": "bool", + }, + "port": { + "type": "str", + }, + "protocol": { + "type": "str", + "choices": [ + "icmp", + "tcp", + "udp", + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "nat66": { + "type": "dict", + "options": { + "destination": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "disable": { + "type": "bool", + }, + "exclude": { + "type": "bool", + }, + "inbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + }, + }, + "log": { + "type": "bool", + }, + "protocol": { + "type": "str", + }, + "source": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + "prefix": { + "type": "str", + }, + }, + }, + "disable": { + "type": "bool", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "outbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + }, + }, + "protocol": { + "type": "str", + }, + "source": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + "prefix": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py index 6940fb7e..5e10d05b 100644 --- a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py index 0b5814be..ced9f98e 100644 --- a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py index 0a422e89..a939652b 100644 --- a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py @@ -25,10 +25,8 @@ The arg spec for the vyos_ospfv2 module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py index a59606dd..77a17d79 100644 --- a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py @@ -25,10 +25,8 @@ The arg spec for the vyos_ospfv3 module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -47,6 +45,12 @@ class Ospfv3Args(object): # pylint: disable=R0903 "area_id": {"type": "str"}, "export_list": {"type": "str"}, "import_list": {"type": "str"}, + "interface": { + "aliases": ["interfaces"], + "type": "list", + "elements": "dict", + "options": {"name": {"type": "str"}}, + }, "range": { "elements": "dict", "options": { diff --git a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py index b01a3e82..c74400ba 100644 --- a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py index 196db0c7..58ffa3e5 100644 --- a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# @@ -74,6 +73,8 @@ class Route_mapsArgs(object): # pylint: disable=R0903 }, "extcommunity_rt": {"type": "str"}, "extcommunity_soo": {"type": "str"}, + "extcommunity_bandwidth": {"type": "str"}, + "extcommunity_bandwidth_non_transitive": {"type": "bool"}, "ip_next_hop": {"type": "str"}, "ipv6_next_hop": { "type": "dict", @@ -100,6 +101,7 @@ class Route_mapsArgs(object): # pylint: disable=R0903 "src": {"type": "str"}, "tag": {"type": "str"}, "weight": {"type": "str"}, + "table": {"type": "str"}, }, }, "match": { @@ -178,6 +180,23 @@ class Route_mapsArgs(object): # pylint: disable=R0903 "next_hop": {"type": "str"}, }, }, + "protocol": { + "type": "str", + "choices": [ + "babel", + "bgp", + "connected", + "isis", + "kernel", + "ospf", + "ospfv3", + "rip", + "ripng", + "static", + "table", + "vnc", + ], + }, "large_community_large_community_list": { "type": "str", }, diff --git a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py index b94c2639..53516c4f 100644 --- a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py index 365df48a..d8a4f11a 100644 --- a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py @@ -25,10 +25,8 @@ The arg spec for the vyos_static_routes module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..75fc1ce5 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec module +""" + + +class Vpn_ipsecArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vpn_ipsec module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "ike_group": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "close_action": { + "type": "str", + "choices": ["none", "trap", "start"], + }, + "dead_peer_detection": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": ["trap", "clear", "restart"], + }, + "interval": {"type": "int"}, + "timeout": {"type": "int"}, + }, + }, + "disable_mobike": {"type": "bool"}, + "ikev2_reauth": {"type": "bool"}, + "key_exchange": { + "type": "str", + "choices": ["ikev1", "ikev2"], + }, + "lifetime": {"type": "int"}, + "mode": {"type": "str", "choices": ["main", "aggressive"]}, + "proposal": { + "type": "list", + "elements": "dict", + "options": { + "proposal_id": {"type": "int"}, + "dh_group": {"type": "int"}, + "encryption": {"type": "str"}, + "hash": {"type": "str"}, + "prf": {"type": "str"}, + }, + }, + }, + }, + "esp_group": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "compression": {"type": "bool"}, + "disable_rekey": {"type": "bool"}, + "life_bytes": {"type": "int"}, + "life_packets": {"type": "int"}, + "lifetime": {"type": "int"}, + "mode": { + "type": "str", + "choices": ["tunnel", "transport"], + }, + "pfs": {"type": "str"}, + "proposal": { + "type": "list", + "elements": "dict", + "options": { + "proposal_id": {"type": "int"}, + "encryption": {"type": "str"}, + "hash": {"type": "str"}, + }, + }, + }, + }, + "authentication": { + "type": "dict", + "options": { + "psk": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "id": {"type": "list", "elements": "str"}, + "dhcp_interface": { + "type": "list", + "elements": "str", + }, + "secret": {"type": "str", "no_log": True}, + "secret_type": { + "type": "str", + "choices": ["base64", "hex", "plaintext"], + }, + }, + }, + "ppk": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "id": {"type": "list", "elements": "str"}, + "secret": {"type": "str", "no_log": True}, + "secret_type": { + "type": "str", + "choices": ["base64", "hex", "plaintext"], + }, + }, + }, + }, + }, + "profile": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "authentication": { + "type": "dict", + "options": { + "mode": { + "type": "str", + "choices": ["pre-shared-secret"], + }, + "pre_shared_secret": {"type": "str", "no_log": True}, + }, + }, + "bind_tunnel": {"type": "list", "elements": "str"}, + "disable": {"type": "bool"}, + "esp_group": {"type": "str"}, + "ike_group": {"type": "str"}, + }, + }, + "interface": {"type": "list", "elements": "str"}, + "log": { + "type": "dict", + "options": { + "level": {"type": "int"}, + "subsystem": { + "type": "list", + "elements": "str", + }, + }, + }, + "options": { + "type": "dict", + "options": { + "disable_route_autoinstall": {"type": "bool"}, + "flexvpn": {"type": "bool"}, + "interface": {"type": "str"}, + "retransmission": { + "type": "dict", + "options": { + "attempts": {"type": "int"}, + "base": {"type": "float"}, + "timeout": {"type": "int"}, + }, + }, + "virtual_ip": {"type": "bool"}, + }, + }, + "disable_uniqreqids": {"type": "bool"}, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..bd23d4d3 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec_s2s module +""" + + +class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vpn_ipsec_s2s module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "peer": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "disable": {"type": "bool"}, + "authentication": { + "type": "dict", + "options": { + "local_id": {"type": "str"}, + "ppk": { + "type": "dict", + "options": { + "id": {"type": "str"}, + "required": {"type": "bool"}, + }, + }, + "rsa": { + "type": "dict", + "options": { + "local_key": {"type": "str", "no_log": True}, + "passphrase": {"type": "str", "no_log": True}, + "remote_key": {"type": "str", "no_log": True}, + }, + }, + "x509": { + "type": "dict", + "options": { + "certificate": {"type": "str"}, + "passphrase": {"type": "str", "no_log": True}, + "ca_certificate": { + "type": "list", + "elements": "str", + }, + }, + }, + "mode": { + "type": "str", + "choices": [ + "pre-shared-secret", + "rsa", + "x509", + ], + }, + "remote_id": {"type": "str"}, + "use_x509_id": {"type": "bool"}, + }, + }, + "childless": { + "type": "str", + "choices": ["allow", "prefer", "force", "never"], + }, + "connection_type": { + "type": "str", + "choices": ["initiate", "trap", "none"], + }, + "default_esp_group": {"type": "str"}, + "description": {"type": "str"}, + "dhcp_interface": {"type": "str"}, + "force_udp_encapsulation": {"type": "bool"}, + "ike_group": {"type": "str"}, + "ikev2_reauth": { + "type": "str", + "choices": ["yes", "no", "inherit"], + }, + "local_address": {"type": "str"}, + "remote_address": {"type": "list", "elements": "str"}, + "replay_window": {"type": "int"}, + "tunnel": { + "type": "list", + "elements": "dict", + "options": { + "tunnel_id": {"type": "int", "required": True}, + "disable": {"type": "bool"}, + "esp_group": {"type": "str"}, + "local": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "protocol": {"type": "str"}, + "priority": {"type": "int"}, + "remote": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + "virtual_address": {"type": "list", "elements": "str"}, + "vti": { + "type": "dict", + "options": { + "bind": {"type": "str"}, + "esp_group": {"type": "str"}, + "traffic_selector": { + "type": "dict", + "options": { + "local": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "remote": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/vrf/__init__.py b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vrf/vrf.py b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py new file mode 100644 index 00000000..20947f9d --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py @@ -0,0 +1,130 @@ +# -*- 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_vrf module +""" + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp_global.bgp_global import ( + Bgp_globalArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv2.ospfv2 import ( + Ospfv2Args, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv3.ospfv3 import ( + Ospfv3Args, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) + + +class VrfArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vrf module""" + + bgp_argument_spec = Bgp_globalArgs.argument_spec["config"] + static_routes_argument_spec = Static_routesArgs.argument_spec["config"] + ospfv2_argument_spec = Ospfv2Args.argument_spec["config"] + ospfv3_argument_spec = Ospfv3Args.argument_spec["config"] + + argument_spec = { + "config": { + "type": "dict", + "options": { + "bind_to_all": {"type": "bool", "default": False}, + "instances": { + "type": "list", + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "description": {"type": "str"}, + "disable": { + "aliases": ["disabled"], + "default": False, + "type": "bool", + }, + "table_id": {"type": "int"}, + "vni": {"type": "int"}, + "address_family": { + "type": "list", + "elements": "dict", + "options": { + "afi": { + "type": "str", + "choices": ["ipv4", "ipv6"], + }, + "disable_forwarding": {"type": "bool", "default": False}, + "nht_no_resolve_via_default": {"type": "bool", "default": False}, + "route_maps": { + "type": "list", + "elements": "dict", + "options": { + "rm_name": {"type": "str", "required": True}, + "protocol": { + "type": "str", + "choices": [ + "any", + "babel", + "bgp", + "eigrp", + "isis", + "ospf", + "rip", + "static", + ], + }, + }, + }, + }, + }, + "protocols": { + # "type": "list", # sanity + # "elements": "dict", + "type": "dict", + "options": { + "bgp": bgp_argument_spec, + "ospf": ospfv2_argument_spec, + "ospfv3": ospfv3_argument_spec, + "static": static_routes_argument_spec, + }, + }, + }, + }, + }, + }, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "replaced", + "overridden", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + "running_config": {"type": "str"}, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py index 0e6bec81..f1494698 100644 --- a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -32,15 +30,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family import ( Bgp_address_familyTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( Bgp_address_familyTemplate14, ) - +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 -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_address_family(ResourceModule): """ @@ -65,7 +62,7 @@ class Bgp_address_family(ResourceModule): self._tmplt = Bgp_address_familyTemplate() def parse(self): - """ override parse to check template """ + """override parse to check template""" self._validate_template() return super().parse() @@ -93,9 +90,11 @@ class Bgp_address_family(ResourceModule): wantd = {} haved = {} - if (self.want.get("as_number") == self.have.get("as_number") or - not self.have or - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")): + if ( + self.want.get("as_number") == self.have.get("as_number") + or not self.have + or LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): if self.want: wantd = {self.want["as_number"]: self.want} if self.have: @@ -113,16 +112,16 @@ class Bgp_address_family(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - for k, have in iteritems(haved): + for k, have in haved.items(): self._delete_af(wantd, have) wantd = {} if self.state == "overridden": - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -149,23 +148,23 @@ class Bgp_address_family(ResourceModule): def _compare_af(self, want, have): waf = want.get("address_family", {}) haf = have.get("address_family", {}) - for name, entry in iteritems(waf): + for name, entry in waf.items(): self._compare_lists( entry, have=haf.get(name, {}), as_number=want["as_number"], afi=name, ) - for name, entry in iteritems(haf): + for name, entry in haf.items(): if name not in waf.keys() and self.state == "replaced": continue self._compare_lists({}, entry, as_number=have["as_number"], afi=name) def _delete_af(self, want, have): - for as_num, entry in iteritems(want): - for afi, af_entry in iteritems(entry.get("address_family", {})): + for as_num, entry in want.items(): + for afi, af_entry in entry.get("address_family", {}).items(): if have.get("address_family"): - for hafi, hentry in iteritems(have["address_family"]): + for hafi, hentry in have["address_family"].items(): if hafi == afi: self.commands.append( self._tmplt.render( @@ -177,9 +176,9 @@ class Bgp_address_family(ResourceModule): True, ), ) - for neigh, neigh_entry in iteritems(entry.get("neighbors", {})): + for neigh, neigh_entry in entry.get("neighbors", {}).items(): if have.get("neighbors"): - for hneigh, hnentry in iteritems(have["neighbors"]): + for hneigh, hnentry in have["neighbors"].items(): if hneigh == neigh: if not neigh_entry.get("address_family"): self.commands.append( @@ -239,9 +238,9 @@ class Bgp_address_family(ResourceModule): ] wneigh = want.get("neighbors", {}) hneigh = have.get("neighbors", {}) - for name, entry in iteritems(wneigh): - for afi, af_entry in iteritems(entry.get("address_family")): - for k, val in iteritems(af_entry): + for name, entry in wneigh.items(): + for afi, af_entry in entry.get("address_family").items(): + for k, val in af_entry.items(): w = { "as_number": want["as_number"], "neighbors": { @@ -268,7 +267,7 @@ class Bgp_address_family(ResourceModule): want=w, have=h, ) - for name, entry in iteritems(hneigh): + for name, entry in hneigh.items(): if name not in wneigh.keys(): # remove surplus config for overridden and replaced if self.state != "replaced": @@ -284,9 +283,9 @@ class Bgp_address_family(ResourceModule): ) continue - for hafi, haf_entry in iteritems(entry.get("address_family")): + for hafi, haf_entry in entry.get("address_family").items(): # remove surplus configs for given neighbor - replace and overridden - for k, val in iteritems(haf_entry): + for k, val in haf_entry.items(): h = { "as_number": have["as_number"], "neighbors": { @@ -317,7 +316,7 @@ class Bgp_address_family(ResourceModule): for attrib in ["redistribute", "networks", "aggregate_address"]: wdict = want.pop(attrib, {}) hdict = have.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): if entry != hdict.get(key, {}): self.compare( parsers=parsers, @@ -348,7 +347,7 @@ class Bgp_address_family(ResourceModule): + attrib, ) hdict = {} - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): self.compare( parsers=parsers, want={}, @@ -358,7 +357,7 @@ class Bgp_address_family(ResourceModule): }, ) # de-duplicate child commands if parent command is present - for val in (self.commands): + for val in self.commands: for val2 in self.commands: if val != val2 and val2.startswith(val): self.commands.remove(val2) @@ -366,13 +365,11 @@ class Bgp_address_family(ResourceModule): def _compare_asn(self, want, have): if want.get("as_number") and not have.get("as_number"): self.commands.append( - "set protocols bgp " - + "system-as " - + str(want.get("as_number")), + "set protocols bgp " + "system-as " + str(want.get("as_number")), ) def _bgp_af_list_to_dict(self, entry): - for name, proc in iteritems(entry): + for name, proc in entry.items(): if "address_family" in proc: af_dict = {} for entry in proc.get("address_family"): diff --git a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py index 91a5af12..2d7d3e63 100644 --- a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -18,7 +17,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -118,10 +116,10 @@ class Bgp_global(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "purged": h_del = {} - for k, v in iteritems(haved): + for k, v in haved.items(): if k in wantd or not wantd: h_del.update({k: v}) - for num, entry in iteritems(h_del): + for num, entry in h_del.items(): self.commands.append(self._tmplt.render({"as_number": num}, "router", True)) wantd = {} @@ -129,7 +127,7 @@ class Bgp_global(ResourceModule): self._compare(want={}, have=self.have) wantd = {} - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -144,7 +142,7 @@ class Bgp_global(ResourceModule): parsers = ["maximum_paths", "timers"] self._compare_neighbor(want, have) self._compare_bgp_params(want, have) - for name, entry in iteritems(want): + for name, entry in want.items(): if name != "as_number": self.compare( parsers=parsers, @@ -154,7 +152,7 @@ class Bgp_global(ResourceModule): name: have.pop(name, {}), }, ) - for name, entry in iteritems(have): + for name, entry in have.items(): if name != "as_number": self.compare( parsers=parsers, @@ -217,7 +215,7 @@ class Bgp_global(ResourceModule): hneigh = have.pop("neighbor", {}) self._compare_neigh_lists(wneigh, hneigh) - for name, entry in iteritems(wneigh): + for name, entry in wneigh.items(): for k, v in entry.items(): if k == "address": continue @@ -233,7 +231,7 @@ class Bgp_global(ResourceModule): }, have={"as_number": want["as_number"], "neighbor": h}, ) - for name, entry in iteritems(hneigh): + for name, entry in hneigh.items(): if name not in wneigh.keys(): if self._check_af(name): msg = "Use the _bgp_address_family module to delete the address_family under neighbor {0}, before replacing/deleting the neighbor.".format( @@ -281,7 +279,7 @@ class Bgp_global(ResourceModule): wbgp = want.pop("bgp_params", {}) hbgp = have.pop("bgp_params", {}) - for name, entry in iteritems(wbgp): + for name, entry in wbgp.items(): if name == "confederation": if entry != hbgp.pop(name, {}): self.addcmd( @@ -325,7 +323,7 @@ class Bgp_global(ResourceModule): if not wbgp and hbgp: self.commands.append("delete protocols bgp" + self._asn_mod + " parameters") hbgp = {} - for name, entry in iteritems(hbgp): + for name, entry in hbgp.items(): if name == "confederation": self.commands.append( "delete protocols bgp" + self._asn_mod + " parameters confederation", @@ -362,7 +360,7 @@ class Bgp_global(ResourceModule): ]: wdict = want.pop(attrib, {}) hdict = have.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): if entry != hdict.pop(key, {}): self.addcmd(entry, "neighbor.{0}".format(attrib), False) # remove remaining items in have for replaced @@ -370,7 +368,7 @@ class Bgp_global(ResourceModule): self.addcmd(entry, "neighbor.{0}".format(attrib), True) def _bgp_global_list_to_dict(self, entry): - for name, proc in iteritems(entry): + for name, proc in entry.items(): if "neighbor" in proc: neigh_dict = {} for entry in proc.get("neighbor", []): diff --git a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py index 34dc0ed6..289037ef 100644 --- a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -28,12 +27,16 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.u from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( + in_target_not_none, list_diff_want_only, ) - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion +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, + load_config, +) class Firewall_global(ConfigBase): @@ -74,6 +77,13 @@ class Firewall_global(ConfigBase): warnings = list() commands = list() + try: + self._module.params["comment"] + except KeyError: + comment = [] + else: + comment = self._module.params["comment"] + if self.state in self.ACTION_STATES: existing_firewall_global_facts = self.get_firewall_global_facts() else: @@ -82,6 +92,12 @@ class Firewall_global(ConfigBase): if self.state in self.ACTION_STATES or self.state == "rendered": commands.extend(self.set_config(existing_firewall_global_facts)) + if commands and self._module._diff: + commit = not self._module.check_mode + diff = load_config(self._module, commands, commit=commit, comment=comment) + if diff: + result["diff"] = {"prepared": str(diff)} + if commands and self.state in self.ACTION_STATES: if not self._module.check_mode: self._connection.edit_config(commands) @@ -189,7 +205,7 @@ class Firewall_global(ConfigBase): "twa_hazards_protection", ) if want: - for key, val in iteritems(want): + for key, val in want.items(): if val and key in b_set and not have: commands.append(self._form_attr_cmd(attr=key, opr=False)) elif val and key in b_set and have and key in have and have[key] != val: @@ -199,7 +215,7 @@ class Firewall_global(ConfigBase): elif not want and have: commands.append(self._compute_command(opr=False)) elif have: - for key, val in iteritems(have): + for key, val in have.items(): if val and key in b_set: commands.append(self._form_attr_cmd(attr=key, opr=False)) else: @@ -225,6 +241,8 @@ class Firewall_global(ConfigBase): commands.extend(self._render_state_policy(key, w, h, opr=opr)) elif key == "route_redirects": commands.extend(self._render_route_redirects(key, w, h, opr=opr)) + elif key == "zone": + commands.extend(self._render_zone(key, w, h, opr=opr)) return commands def _add_global_attr(self, w, h, opr=True): @@ -246,7 +264,7 @@ class Firewall_global(ConfigBase): "twa_hazards_protection", ) if w_fg: - for key, val in iteritems(w_fg): + for key, val in w_fg.items(): if opr and key in l_set and not (h and self._is_w_same(w_fg, h, key)): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), @@ -257,11 +275,7 @@ class Firewall_global(ConfigBase): self._form_attr_cmd(attr=key, key=self._bool_to_str(val), opr=opr), ) continue - if ( - key in l_set - and not self._in_target(h, key) - and not self._is_del(l_set, h) - ): + if key in l_set and not self._in_target(h, key) and not self._is_del(l_set, h): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), ) @@ -284,13 +298,13 @@ class Firewall_global(ConfigBase): if h: h_ping = h.get(attr) or {} if self._is_root_del(w[attr], h_ping, attr): - for item, value in iteritems(h[attr]): + for item, value in h[attr].items(): if not opr and item in l_set: commands.append(self._form_attr_cmd(attr=item, opr=opr)) elif w[attr]: if h and attr in h.keys(): h_ping = h.get(attr) or {} - for item, value in iteritems(w[attr]): + for item, value in w[attr].items(): if ( opr and item in l_set @@ -354,7 +368,7 @@ class Firewall_global(ConfigBase): cmd = self._compute_command(key="group", attr="ipv6-" + attr, opr=opr) else: cmd = self._compute_command(key="group", attr=attr, opr=opr) - for key, val in iteritems(want): + for key, val in want.items(): if val: if opr and key in l_set and not (h and self._is_w_same(want, h, key)): if key == "name": @@ -372,12 +386,18 @@ class Firewall_global(ConfigBase): ) elif not opr and key in l_set: if key == "name" and self._is_grp_del(h, want, key): + if len(commands) > 0 and commands[-1] == cmd + " " + want[ + "name" + ] + " " + self._grp_type( + attr, + ): + commands.pop() commands.append(cmd + " " + want["name"]) continue - if not (h and self._in_target(h, key)) and not self._is_grp_del( + if not (h and in_target_not_none(h, key)) and not self._is_grp_del( h, want, - key, + "name", ): commands.append(cmd + " " + want["name"] + " " + key) elif key == "members": @@ -438,6 +458,10 @@ class Firewall_global(ConfigBase): + " " + member[self._get_mem_type(type)], ) + elif not opr and not have: + commands.append( + cmd + " " + name + " " + self._grp_type(type), + ) return commands def _get_mem_type(self, group): @@ -473,10 +497,12 @@ class Firewall_global(ConfigBase): if want: for w in want: h = self.search_attrib_in_have(have, w, "connection_type") - for key, val in iteritems(w): + for key, val in w.items(): if val and key != "connection_type": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): - if key == "log" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if key == "log" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): commands.append( self._form_attr_cmd( key=attr + " " + w["connection_type"], @@ -502,7 +528,9 @@ class Firewall_global(ConfigBase): ), ) break # delete the whole thing and move on - if (not self._in_target(h, key) or h[key] is None) and (self._in_target(w, key) and w[key]): + if (not self._in_target(h, key) or h[key] is None) and ( + self._in_target(w, key) and w[key] + ): # delete if not being replaced and value currently exists commands.append( self._form_attr_cmd( @@ -534,23 +562,23 @@ class Firewall_global(ConfigBase): if want: for w in want: h = self.search_attrib_in_have(have, w, "afi") - if 'afi' in w: - afi = w['afi'] + if "afi" in w: + afi = w["afi"] else: - if h and 'afi' in h: - afi = h['afi'] + if h and "afi" in h: + afi = h["afi"] else: afi = None afi = None - for key, val in iteritems(w): - if val and key != "afi": + for key, val in w.items(): + if val is not None and key != "afi": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): commands.append( self._form_attr_cmd( attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) elif not opr and key in l_set: @@ -560,7 +588,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) continue @@ -570,7 +598,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) elif key == "icmp_redirects": @@ -590,20 +618,25 @@ class Firewall_global(ConfigBase): commands = [] h_red = {} l_set = ("send", "receive") - if w and 'afi' in w: - afi = w['afi'] + if w and "afi" in w: + afi = w["afi"] else: - if h and 'afi' in h: - afi = h['afi'] + if h and "afi" in h: + afi = h["afi"] else: afi = None if w[attr]: if h and attr in h.keys(): h_red = h.get(attr) or {} - for item, value in iteritems(w[attr]): + for item, value in w[attr].items(): if opr and item in l_set and not (h_red and self._is_w_same(w[attr], h_red, item)): commands.append( - self._form_attr_cmd(attr=item, val=self._bool_to_str(value), opr=opr, type=afi) + self._form_attr_cmd( + attr=item, + val=self._bool_to_str(value), + opr=opr, + type=afi, + ), ) elif ( not opr @@ -637,7 +670,12 @@ class Firewall_global(ConfigBase): :param type: AF type of attribute. :return: generated command. """ - command = self._compute_command(key=key, attr=self._map_attrib(attr, type=type), val=val, opr=opr) + command = self._compute_command( + key=key, + attr=self._map_attrib(attr, type=type), + val=val, + opr=opr, + ) return command def _compute_command(self, key=None, attr=None, val=None, remove=False, opr=True): @@ -654,14 +692,20 @@ class Firewall_global(ConfigBase): cmd = "delete firewall " else: cmd = "set firewall " - if attr and key != "group" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if ( + attr + and key not in ["group", "zone"] + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): cmd += "global-options " if key: cmd += key.replace("_", "-") + " " if attr: cmd += attr.replace("_", "-") if val and opr: - if key == "state_policy" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if key == "state_policy" and LooseVersion(get_os_version(self._module)) >= LooseVersion( + "1.4", + ): cmd += "" else: cmd += " '" + str(val) + "'" @@ -768,3 +812,324 @@ class Firewall_global(ConfigBase): elif attrib == "validation": regex = "source-validation" return regex + + def _render_zone(self, attr, w, h, opr): + """ + This function forms the commands for group list/members attributes based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param opr: True/False. + :return: generated list of commands. + """ + commands = [] + h_grp = [] + w_grp = [] + l_set = ("name", "description", "default_action", "default_log", "local_zone") + if w: + w_grp = w.get(attr) or [] + if h: + h_grp = h.get(attr) or [] + + if w_grp: + for want in w_grp: + h = self.search_attrib_in_have(h_grp, want, "name") + + cmd = self._compute_command(key="zone", attr="", opr=opr) + + if not opr and self._is_grp_del(h, want, "name"): + commands.append(cmd + " " + want["name"]) + continue + + for key, val in want.items(): + if val: + if opr and key in l_set and not (h and self._is_w_same(want, h, key)): + if key == "name": + pass + elif isinstance(val, bool): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + else: + commands.append( + cmd + + " " + + want["name"] + + " " + + key.replace("_", "-") + + " '" + + str(want[key]) + + "'", + ) + elif not opr and key in l_set: + if not (h and in_target_not_none(h, key)) and not self._is_grp_del( + h, + want, + "name", + ): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + elif key == "interfaces": + commands.extend( + self._render_interfaces( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "intra_zone_filtering": + commands.extend( + self._render_izf( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "sources": + commands.extend( + self._render_sources( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + return commands + + def _render_interfaces(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for interfaces + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + # VyOS 1.5.0 GA moved 'interface' under a new 'member' node + # ("set firewall zone <name> member interface <ifname>"). 1.4.x and + # 1.5-rolling snapshots predating this change still use the bare + # 'interface' node. Known limitation: a 1.5-rolling build reporting + # "1.5" that predates this change will incorrectly get the new + # syntax -- accepted trade-off, see PR notes. + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"): + iface_kw = "member interface" + else: + iface_kw = "interface" + + if want: + if opr: + interfaces = list_diff_want_only(want, have) + + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and have: + interfaces = list_diff_want_only(want, have) + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and not have: + for interface in want: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + else: + self._module.fail_json(msg={"want": want, "have": have, "opr": opr}) + + return commands + + def _render_izf(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for intra zone filtering + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + if want: + if opr: + izfs = self._dict_diff(want, have) + for izf in izfs: + commands.append( + cmd + + " " + + name + + " intra-zone-filtering " + + izf[0].replace(".", " ") + + " " + + izf[1], + ) + elif not opr and have: + izfs = self._dict_diff(want, have) + + for izf in izfs: + commands.append( + cmd + " " + name + " intra-zone-filtering " + izf[0].replace(".", " "), + ) + elif not opr and not have: + commands.append( + cmd + " " + name + " intra-zone-filtering", + ) + return commands + + def _dict_diff(self, want, have, path=""): + """ + Recursively find keys/values in `want` that differ or are missing in `have`. + Returns list of tuples: (full_path, value_in_want) + """ + diffs = [] + + have = have or {} + + for key, want_val in want.items(): + current_path = f"{path}.{key.replace('_', '-')}" if path else key.replace("_", "-") + + if key not in have: + if isinstance(want_val, dict): + diffs.extend(self._dict_diff(want_val, {}, current_path)) + elif isinstance(want_val, list): + for i, item in enumerate(want_val): + if isinstance(item, dict): + diffs.extend(self._dict_diff(item, {}, f"{current_path}[{i}]")) + else: + diffs.append((f"{current_path}[{i}]", item)) + else: + diffs.append((current_path, want_val)) + + else: + have_val = have[key] + + if isinstance(want_val, dict) and isinstance(have_val, dict): + diffs.extend(self._dict_diff(want_val, have_val, current_path)) + + elif isinstance(want_val, list) and isinstance(have_val, list): + for i, item in enumerate(want_val): + if i >= len(have_val): + diffs.append((f"{current_path}[{i}]", item)) + elif isinstance(item, dict) and isinstance(have_val[i], dict): + diffs.extend( + self._dict_diff(item, have_val[i], f"{current_path}[{i}]"), + ) + elif item != have_val[i]: + diffs.append((f"{current_path}[{i}]", item)) + + elif want_val != have_val: + diffs.append((current_path, want_val)) + + return diffs + + def _render_sources(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for sources (from) + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + have_index = {item["zone"]: item for item in have} + + for item1 in want: + zone = item1["zone"] + + if zone in have_index: + item2 = have_index[zone] + + wfw = item1.get("firewall", {}) + hfw = item2.get("firewall", {}) + if wfw: + if opr: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-") + + " " + + source[1], + ) + elif not opr and hfw: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-"), + ) + elif not opr and not hfw: + commands.append( + cmd + " " + name + " from " + zone, + ) + elif opr: + wfw = item1.get("firewall", {}) + for key, val in wfw.items(): + if val: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + key.replace("_", "-") + + " " + + val, + ) + elif not opr: + commands.append( + cmd + " " + name + " from " + zone, + ) + return commands diff --git a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py index 85a8042f..ec1aaef1 100644 --- a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py +++ b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py @@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py index 2942b191..be36d3fe 100644 --- a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -30,10 +29,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( list_diff_want_only, ) - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion +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, + load_config, +) class Firewall_rules(ConfigBase): @@ -78,6 +80,14 @@ class Firewall_rules(ConfigBase): result = {"changed": False} warnings = list() commands = list() + diff = None + + try: + self._module.params["comment"] + except KeyError: + comment = [] + else: + comment = self._module.params["comment"] if self.state in self.ACTION_STATES: existing_firewall_rules_facts = self.get_firewall_rules_facts() @@ -87,6 +97,12 @@ class Firewall_rules(ConfigBase): if self.state in self.ACTION_STATES or self.state == "rendered": commands.extend(self.set_config(deepcopy(existing_firewall_rules_facts))) + if commands and self._module._diff: + commit = not self._module.check_mode + diff = load_config(self._module, commands, commit=commit, comment=comment) + if diff: + result["diff"] = {"prepared": str(diff)} + if commands and self.state in self.ACTION_STATES: if not self._module.check_mode: self._connection.edit_config(commands) @@ -213,13 +229,17 @@ class Firewall_rules(ConfigBase): commands.append(self._compute_command(rs_id, remove=True)) # Blank out the only rule set that it is removed. for entry in have: - if entry['afi'] == rs_id['afi'] and rs_id['name']: + if entry["afi"] == rs_id["afi"] and rs_id["name"]: entry["rule_sets"] = [ - rule_set for rule_set in entry["rule_sets"] if rule_set.get("name") != rs_id['name'] + rule_set + for rule_set in entry["rule_sets"] + if rule_set.get("name") != rs_id["name"] ] - elif entry['afi'] == rs_id['afi'] and rs_id['filter']: + elif entry["afi"] == rs_id["afi"] and rs_id["filter"]: entry["rule_sets"] = [ - rule_set for rule_set in entry["rule_sets"] if rule_set.get("filter") != rs_id['filter'] + rule_set + for rule_set in entry["rule_sets"] + if rule_set.get("filter") != rs_id["filter"] ] commands.extend(self._state_merged(want, have)) return commands @@ -264,7 +284,7 @@ class Firewall_rules(ConfigBase): for h in have: if h["afi"] == w["afi"]: commands.append( - self._compute_command(self._rs_id(None, w["afi"]), remove=True) + self._compute_command(self._rs_id(None, w["afi"]), remove=True), ) elif have: for h in have: @@ -294,7 +314,7 @@ class Firewall_rules(ConfigBase): h_rs = deepcopy(remove_empties(have)) h_rules = h_rs.pop("rules", None) if w_rs: - for key, val in iteritems(w_rs): + for key, val in w_rs.items(): if opr and key in l_set and not (h_rs and self._is_w_same(w_rs, h_rs, key)): if key == "enable_default_log": if val and (not h_rs or key not in h_rs or not h_rs[key]): @@ -338,6 +358,7 @@ class Firewall_rules(ConfigBase): "disable", "description", "jump_target", + "offload_target", ) if w_rules: for w in w_rules: @@ -345,7 +366,7 @@ class Firewall_rules(ConfigBase): h = self.search_rules_in_have_rs(h_rules, w["number"]) if w != h and self.state == "replaced": h = {} - for key, val in iteritems(w): + for key, val in w.items(): if val: if opr and key in l_set and not (h and self._is_w_same(w, h, key)): if key == "disable": @@ -443,7 +464,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_state = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -452,7 +473,9 @@ class Firewall_rules(ConfigBase): if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): commands.append(cmd + (" " + attr + " " + item)) else: - commands.append(cmd + (" " + attr + " " + item + " " + self._bool_to_str(val))) + commands.append( + cmd + (" " + attr + " " + item + " " + self._bool_to_str(val)), + ) elif not opr and item in l_set and not self._in_target(h_state, item): commands.append(cmd + (" " + attr + " " + item)) return commands @@ -504,7 +527,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_recent = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -532,7 +555,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_icmp = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -555,7 +578,9 @@ class Firewall_rules(ConfigBase): else: commands.append(cmd + (" " + attr + " " + item + " " + str(val))) elif not opr and item in l_set and not self._in_target(h_icmp, item): - commands.append(cmd + (" " + attr + " " + item.replace("_", "-") + " " + str(val))) + commands.append( + cmd + (" " + attr + " " + item.replace("_", "-") + " " + str(val)), + ) return commands def _add_interface(self, attr, w, h, cmd, opr): @@ -573,15 +598,15 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_if = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if opr and item in l_set and not (h_if and self._is_w_same(w[attr], h_if, item)): commands.append( cmd - + (" " + attr.replace("_", "-") + " " + item.replace("_", "-") + " " + val) + + (" " + attr.replace("_", "-") + " " + item.replace("_", "-") + " " + val), ) elif not opr and item in l_set and not (h_if and self._in_target(h_if, item)): commands.append( - cmd + (" " + attr.replace("_", "-") + " " + item.replace("_", "-")) + cmd + (" " + attr.replace("_", "-") + " " + item.replace("_", "-")), ) return commands @@ -608,7 +633,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_time = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -654,14 +679,14 @@ class Firewall_rules(ConfigBase): for flag in flags: invert = flag.get("invert", False) commands.append( - cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]), ) elif not opr: flags = list_diff_want_only(want, have) for flag in flags: invert = flag.get("invert", False) commands.append( - cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]), ) return commands @@ -837,7 +862,7 @@ class Firewall_rules(ConfigBase): h_group = {} if h and h.get(attr) and key in h[attr].keys(): h_group = h[attr].get(key) - for item, val in iteritems(group): + for item, val in group.items(): if val: if ( opr @@ -969,7 +994,10 @@ class Firewall_rules(ConfigBase): if number: cmd += " rule " + str(number) if attrib: - if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") and attrib == "enable_default_log": + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attrib == "enable_default_log" + ): cmd += " " + "default-log" else: cmd += " " + attrib.replace("_", "-") @@ -1107,15 +1135,23 @@ class Firewall_rules(ConfigBase): for item in rs: self._prune_stubs(item) elif isinstance(rs, dict): - keys_to_remove = [key for key, value in rs.items() - if ( - (key == "disable" and value is False) - or - (key == "log" and value == "disable" and - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")) - or - (key in ["new", "invalid", "related", "established"] and value is False and - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")))] + keys_to_remove = [ + key + for key, value in rs.items() + if ( + (key == "disable" and value is False) + or ( + key == "log" + and value == "disable" + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ) + or ( + key in ["new", "invalid", "related", "established"] + and value is False + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ) + ) + ] for key in keys_to_remove: del rs[key] for key in rs: @@ -1131,10 +1167,20 @@ class Firewall_rules(ConfigBase): return True elif isinstance(w, list) and isinstance(rs, list): try: - sorted_list1 = sorted(w, key=lambda x: str(x)) # pylint: disable=unnecessary-lambda - sorted_list2 = sorted(rs, key=lambda x: str(x)) # pylint: disable=unnecessary-lambda + + def comparison(x): + if "name" in x: + return x["name"] + if "number" in x: + return x["number"] + return str(x) + + sorted_list1 = sorted(w, key=comparison) + sorted_list2 = sorted(rs, key=comparison) except TypeError: return False + if len(sorted_list1) != len(sorted_list2): + return False return all(self._is_same_rs(x, y) for x, y in zip(sorted_list1, sorted_list2)) else: return w == rs 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/config/hostname/hostname.py b/plugins/module_utils/network/vyos/config/hostname/hostname.py index 36aba74f..8b30a693 100644 --- a/plugins/module_utils/network/vyos/config/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/config/hostname/hostname.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py index a9d9307c..71e4c0d1 100644 --- a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py @@ -11,12 +11,10 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -76,6 +74,7 @@ class Interfaces(ConfigBase): if self.state in self.ACTION_STATES: existing_interfaces_facts = self.get_interfaces_facts() + else: existing_interfaces_facts = [] @@ -227,7 +226,7 @@ class Interfaces(ConfigBase): updates = dict_diff(have_copy, want_copy) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): commands.append( self._compute_commands(key=key, value=value, interface=want_copy["name"]), ) @@ -243,7 +242,7 @@ class Interfaces(ConfigBase): vif_updates = dict_diff(have_vif, want_vif) if vif_updates: - for key, value in iteritems(vif_updates): + for key, value in vif_updates.items(): commands.append( self._compute_commands( key=key, diff --git a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py index 6e0c005f..cfce6fee 100644 --- a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py @@ -13,13 +13,11 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -80,7 +78,7 @@ class L3_interfaces(ConfigBase): commands = list() if self.state in self.ACTION_STATES: - existing_l3_interfaces_facts = self.get_l3_interfaces_facts() + existing_l3_interfaces_facts = self.mutate_autoconfig(self.get_l3_interfaces_facts()) else: existing_l3_interfaces_facts = [] @@ -96,16 +94,19 @@ class L3_interfaces(ConfigBase): result["commands"] = commands if self.state in self.ACTION_STATES or self.state == "gathered": - changed_l3_interfaces_facts = self.get_l3_interfaces_facts() + changed_l3_interfaces_facts = self.mutate_autoconfig(self.get_l3_interfaces_facts()) elif self.state == "rendered": result["rendered"] = commands elif self.state == "parsed": running_config = self._module.params["running_config"] + if not running_config: self._module.fail_json( msg="value of running_config parameter must not be empty for state parsed", ) - result["parsed"] = self.get_l3_interfaces_facts(data=running_config) + result["parsed"] = self.mutate_autoconfig( + self.get_l3_interfaces_facts(data=running_config), + ) else: changed_l3_interfaces_facts = [] @@ -129,6 +130,7 @@ class L3_interfaces(ConfigBase): """ want = self._module.params["config"] have = existing_l3_interfaces_facts + resp = self.set_state(want, have) return to_list(resp) @@ -174,6 +176,7 @@ class L3_interfaces(ConfigBase): elif state == "replaced": commands.extend(self._state_replaced(item, obj_in_have)) + commands = [command.replace("auto-config", "autoconf") for command in commands] return commands def _state_replaced(self, want, have): @@ -226,7 +229,7 @@ class L3_interfaces(ConfigBase): have_vifs = have_copy.pop("vifs", []) for update in self._get_updates(want_copy, have_copy): - for key, value in iteritems(update): + for key, value in update.items(): commands.append( self._compute_commands(key=key, value=value, interface=want_copy["name"]), ) @@ -238,7 +241,7 @@ class L3_interfaces(ConfigBase): have_vif = {} for update in self._get_updates(want_vif, have_vif): - for key, value in iteritems(update): + for key, value in update.items(): commands.append( self._compute_commands( key=key, @@ -247,12 +250,15 @@ class L3_interfaces(ConfigBase): vif=want_vif["vlan_id"], ), ) - return commands def _state_deleted(self, want, have): """The command generator when state is deleted + Deletes only the L3 address attributes (base interface and VIFs) + owned by this module, never the interface subtree, so L2 settings + are preserved. + :rtype: A list :returns: the commands necessary to remove the current configuration of the provided objects @@ -261,51 +267,92 @@ class L3_interfaces(ConfigBase): want_copy = deepcopy(remove_empties(want)) have_copy = deepcopy(have) - want_vifs = want_copy.pop("vifs", []) - have_vifs = have_copy.pop("vifs", []) - - for update in self._get_updates(have_copy, want_copy): - for key, value in iteritems(update): - commands.append( - self._compute_commands( - key=key, - value=value, - interface=want_copy["name"], - remove=True, - ), - ) - - if have_vifs: - for have_vif in have_vifs: - want_vif = search_obj_in_list(have_vif["vlan_id"], want_vifs, key="vlan_id") - if not want_vif: - want_vif = {"vlan_id": have_vif["vlan_id"]} + if have_copy is not None: + if all(v in (None, {}, []) for k, v in want_copy.items() if k != "name"): + # Only delete L3 attributes we own — do not touch L2 config + have_vifs = have_copy.pop("vifs", []) or [] - for update in self._get_updates(have_vif, want_vif): - for key, value in iteritems(update): + for addr_family in ("ipv4", "ipv6"): + for addr in have_copy.get(addr_family) or []: commands.append( self._compute_commands( - key=key, + key="address", + value=addr["address"], interface=want_copy["name"], - value=value, - vif=want_vif["vlan_id"], remove=True, ), ) + for have_vif in have_vifs: + for addr_family in ("ipv4", "ipv6"): + for addr in have_vif.get(addr_family) or []: + commands.append( + self._compute_commands( + key="address", + value=addr["address"], + interface=want_copy["name"], + vif=have_vif["vlan_id"], + remove=True, + ), + ) + + return commands + + want_vifs = want_copy.pop("vifs", []) + have_vifs = have_copy.pop("vifs", []) + + if have_vifs: + for have_vif in have_vifs: + want_vif = search_obj_in_list(have_vif["vlan_id"], want_vifs, key="vlan_id") + if not want_vif: + want_vif = {"vlan_id": have_vif["vlan_id"]} + + for update in self._get_updates(have_vif, want_vif): + for key, value in update.items(): + commands.append( + self._compute_commands( + key=key, + interface=want_copy["name"], + value=value, + vif=want_vif["vlan_id"], + remove=True, + ), + ) + + for update in self._get_updates(have_copy, want_copy): + for key, value in update.items(): + commands.append( + self._compute_commands( + key=key, + value=value, + interface=want_copy["name"], + remove=True, + ), + ) + return commands def _compute_commands(self, interface, key, vif=None, value=None, remove=False): - intf_context = "interfaces {0} {1}".format(get_interface_type(interface), interface) + if value == "auto-config" and vif is None: + intf_context = "interfaces {0} {1} ipv6".format( + get_interface_type(interface), + interface, + ) + else: + intf_context = "interfaces {0} {1}".format(get_interface_type(interface), interface) + set_cmd = "set {0}".format(intf_context) del_cmd = "delete {0}".format(intf_context) if vif: - set_cmd = set_cmd + (" vif {0}".format(vif)) - del_cmd = del_cmd + (" vif {0}".format(vif)) + suffix = " ipv6" if value == "auto-config" else "" + set_cmd += f" vif {vif}{suffix}" + del_cmd += f" vif {vif}{suffix}" - if remove: + if remove and key and value: command = "{0} {1} '{2}'".format(del_cmd, key, value) + elif remove and not (key and value): + command = "{0}".format(del_cmd) else: command = "{0} {1} '{2}'".format(set_cmd, key, value) @@ -318,3 +365,12 @@ class L3_interfaces(ConfigBase): updates.extend(diff_list_of_dicts(want.get("ipv6", []), have.get("ipv6", []))) return updates + + def mutate_autoconfig(self, obj): + if isinstance(obj, dict): + return dict(map(lambda kv: (kv[0], self.mutate_autoconfig(kv[1])), obj.items())) + if isinstance(obj, list): + return list(map(self.mutate_autoconfig, obj)) + if isinstance(obj, str): + return obj.replace("autoconf", "auto-config") + return obj diff --git a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py index 6890fe0c..203519e4 100644 --- a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py @@ -8,11 +8,11 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.six import iteritems + from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -245,7 +245,7 @@ class Lag_interfaces(ConfigBase): commands.extend(self._add_bond_members(want, have)) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value: if key == "arp_monitor": commands.extend(self._add_arp_monitor(updates, key, want, have)) diff --git a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py index 1dfd25e4..82a35a54 100644 --- a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py @@ -8,11 +8,11 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.six import iteritems + from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -241,7 +241,7 @@ class Lldp_global(ConfigBase): commands.extend(self._add_management_addresses(want, have)) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value is not None: if key == "enable": if value is False: diff --git a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py index 2fd6a548..36bf1b2d 100644 --- a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py @@ -13,10 +13,8 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -285,7 +283,7 @@ class Lldp_interfaces(ConfigBase): have_dict = have_location_type.get("coordinate_based") or {} location_type = "coordinate-based" updates = dict_diff(have_dict, want_dict) - for key, value in iteritems(updates): + for key, value in updates.items(): if value: commands.append(self._compute_command(set_cmd + location_type, key, str(value))) @@ -319,7 +317,7 @@ class Lldp_interfaces(ConfigBase): if is_dict_element_present(have_location_type, "coordinate_based"): have_dict = have_location_type.get("coordinate_based") or {} location_type = "coordinate-based" - for key, value in iteritems(have_dict): + for key, value in have_dict.items(): only_in_have = key_value_in_dict(key, value, want_dict) if not only_in_have: commands.append( diff --git a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py index f94c9195..1724f338 100644 --- a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -33,6 +31,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import ( Logging_globalTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import ( + Logging_globalTemplate15, +) +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 Logging_global(ResourceModule): @@ -48,22 +53,25 @@ class Logging_global(ResourceModule): resource="logging_global", tmplt=Logging_globalTemplate(), ) - self.parsers = [ - "console.facilities", - "global_params.archive.file_num", - "global_params.archive.size", - "global_params.marker_interval", - "global_params.preserve_fqdn", - "global_params.facilities", - "files.archive.size", - "files.archive.file_num", - "files", - "hosts.port", - "hosts.facility.protocol", # 1.3 and below - "hosts.protocol", - "hosts", - "users", - ] + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.5"): + self._tmplt = Logging_globalTemplate15() + else: + self._tmplt = Logging_globalTemplate() + + self.parsers = [p["name"] for p in self._tmplt.PARSERS if not p["name"].endswith(".state")] + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) def execute_module(self): """Execute the module @@ -71,11 +79,31 @@ class Logging_global(ResourceModule): :rtype: A dictionary :returns: The result from module execution """ + self._validate_template() if self.state not in ["parsed", "gathered"]: self.generate_commands() self.run_commands() return self.result + def _strip_unsupported_15(self, data): + """Remove 1.4-only keys from a list_to_dict result for 1.5 devices.""" + if not data: + return data + warnings = [] + for key in ("files", "users"): + if data.pop(key, None) is not None: + warnings.append( + "'{0}' is not supported on VyOS 1.5+, ignoring.".format(key), + ) + if "global_params" in data: + if data["global_params"].pop("archive", None) is not None: + warnings.append( + "'global_params.archive' is not supported on VyOS 1.5+, ignoring.", + ) + for warning in warnings: + self._module.warn(warning) + return data + def generate_commands(self): """Generate configuration commands to send based on want, have and desired state. @@ -89,10 +117,15 @@ class Logging_global(ResourceModule): else: haved = dict() + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.5"): + wantd = self._strip_unsupported_15(wantd) + haved = self._strip_unsupported_15(haved) + if self.state in ["overridden", "replaced"]: if wantd != haved: wantx, havex = self.call_op(wantd, haved, "overridden") - for k, have in iteritems(havex): + for k, have in havex.items(): if k not in wantx: self._compare(want={}, have=have) @@ -102,7 +135,7 @@ class Logging_global(ResourceModule): if self.state == "merged": wantd = dict_merge(haved, wantd) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -116,12 +149,12 @@ class Logging_global(ResourceModule): def operation_rep(self, params): op_val = dict() - for k, val in iteritems(params): + for k, val in params.items(): if k in ["console", "global_params"]: mod_val = deepcopy(val) op_val.update(self.flatten_facility({k: mod_val})) elif k in ["files", "hosts", "users"]: - for m, n in iteritems(val): + for m, n in val.items(): mod_n = deepcopy(n) if mod_n.get("archive"): del mod_n["archive"] @@ -160,18 +193,18 @@ class Logging_global(ResourceModule): def flatten_facility(self, param): temp_param = dict() - for element, val in iteritems(param): + for element, val in param.items(): if element in ["console", "global_params", "syslog"]: if element != "syslog" and val.get("facilities"): - for k, v in iteritems(val.get("facilities")): + for k, v in val.get("facilities").items(): temp_param[k + element] = {element: {"facilities": v}} del val["facilities"] if val: temp_param[element] = {element: val} if element in ["files", "hosts", "users"]: - for k, v in iteritems(val): + for k, v in val.items(): if v.get("facilities"): - for pk, dat in iteritems(v.get("facilities")): + for pk, dat in v.get("facilities").items(): temp_param[pk + k] = { element: { "facilities": dat, @@ -197,7 +230,7 @@ class Logging_global(ResourceModule): "hosts": "hostname", "users": "username", } - for element, val in iteritems(param): + for element, val in param.items(): if element == "facilities": # only with recursion call _tem_par = {} for par in val: diff --git a/plugins/module_utils/network/vyos/config/nat/__init__.py b/plugins/module_utils/network/vyos/config/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/config/nat/nat.py b/plugins/module_utils/network/vyos/config/nat/nat.py new file mode 100644 index 00000000..a2800914 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/nat.py @@ -0,0 +1,577 @@ +# -*- coding: utf-8 -*- +# 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 + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) + +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.nat import ( + NatTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine + + +class Nat(ResourceModule): + """The vyos_nat config class""" + + def __init__(self, module): + super(Nat, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="nat", + tmplt=NatTemplate(), + ) + self.parsers = [] + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + + if self.state == "merged": + wantd = combine(haved, wantd, recursive=True) + + if self.state == "deleted": + if not wantd: + for nat_type in haved: + self.commands.append(f"delete {nat_type}") + return + self._list_to_dict(wantd) + self._list_to_dict(haved) + self._delete_nat_objects(wantd, haved, only_missing=False) + return + + self._list_to_dict(wantd) + self._list_to_dict(haved) + + if self.state == "replaced": + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + elif self.state == "overridden": + self._delete_nat_objects(wantd, haved, only_missing=True) + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + else: + self._set_commands(wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # List → keyed dict conversion + # ------------------------------------------------------------------------- + + def _list_to_dict(self, config): + nat = config.get("nat", {}) + cgnat = nat.get("cgnat", {}) + + pool = cgnat.get("pool", {}) + for ptype in ("external", "internal"): + entries = pool.get(ptype) + if isinstance(entries, list): + pool[ptype] = {item["name"]: item for item in entries} + + rules = cgnat.get("rule") + if isinstance(rules, list): + cgnat["rule"] = {r["id"]: r for r in rules} + + for section in ("destination", "source", "static"): + rules = nat.get(section, {}).get("rule") + if isinstance(rules, list): + nat[section]["rule"] = {r["id"]: r for r in rules} + + nat64 = config.get("nat64", {}) + rules = nat64.get("source", {}).get("rule") + if isinstance(rules, list): + nat64["source"]["rule"] = {r["id"]: r for r in rules} + for rule in nat64["source"]["rule"].values(): + pools = rule.get("translation", {}).get("pool") + if isinstance(pools, list): + rule["translation"]["pool"] = {p["id"]: p for p in pools} + + nat66 = config.get("nat66", {}) + for section in ("destination", "source"): + rules = nat66.get(section, {}).get("rule") + if isinstance(rules, list): + nat66[section]["rule"] = {r["id"]: r for r in rules} + + # ------------------------------------------------------------------------- + # Top-level dispatch + # ------------------------------------------------------------------------- + + def _set_commands(self, wantd, haved): + self._compare_cgnat_global(wantd, haved) + self._compare_cgnat_pools(wantd, haved) + self._compare_cgnat_rules(wantd, haved) + + for section in ("destination", "source", "static"): + self._compare_nat_rules("nat", section, wantd, haved) + + self._compare_nat_rules("nat64", "source", wantd, haved) + + for section in ("destination", "source"): + self._compare_nat_rules("nat66", section, wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # Delete helpers + # ------------------------------------------------------------------------- + + def _delete_nat_objects(self, wantd, haved, only_missing=False): + """ + Generate delete commands for NAT objects. + only_missing=False: delete objects present in both want and have (when different) + only_missing=True: delete objects present in have but absent from want + """ + for nat_type in haved: + want_nat = wantd.get(nat_type, {}) + have_nat = haved[nat_type] + + if only_missing and nat_type not in wantd: + self.commands.append(f"delete {nat_type}") + continue + + for section in have_nat: + want_section = want_nat.get(section, {}) + have_section = have_nat[section] + + if only_missing and section not in want_nat: + self.commands.append( + f"delete {nat_type} {section.replace('_', '-')}", + ) + continue + + if section == "cgnat": + for pool_type in ("external", "internal"): + want_pools = want_section.get("pool", {}).get(pool_type, {}) + have_pools = have_section.get("pool", {}).get(pool_type, {}) + for name in have_pools: + if only_missing and name not in want_pools: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + + elif not only_missing and name in want_pools: + if self.state == "deleted" or want_pools[name] != have_pools[name]: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + else: + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + cli_section = section.replace("_", "-") + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + # ------------------------------------------------------------------------- + # CGNAT + # ------------------------------------------------------------------------- + + def _compare_cgnat_global(self, wantd, haved): + if self.state in ("replaced", "overridden") and not wantd.get("nat", {}).get("cgnat"): + return + w = wantd.get("nat", {}).get("cgnat", {}).get("log_allocation") + h = haved.get("nat", {}).get("cgnat", {}).get("log_allocation") + if bool(w) != bool(h): + self.addcmd( + {"nat": {"cgnat": {"log_allocation": True}}}, + "cgnat_log_allocation", + not bool(w), + ) + + def _compare_cgnat_pools(self, wantd, haved): + want_ext = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + have_ext = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + want_int = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + have_int = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + + scope = self.state in ("replaced", "overridden") + ext_names = set(want_ext) if scope else set(want_ext) | set(have_ext) + int_names = set(want_int) if scope else set(want_int) | set(have_int) + + for name in ext_names: + w = want_ext.get(name, {}) + h = have_ext.get(name, {}) + if scope and w != h: + h = {} + self._compare_external_pool(name, w, h) + + for name in int_names: + w = want_int.get(name, {}) + h = have_int.get(name, {}) + if scope and w != h: + h = {} + self._compare_internal_pool(name, w, h) + + def _compare_external_pool(self, name, want, have): + w = want.get("external_port_range") + h = have.get("external_port_range") + if w != h: + if w: + self.addcmd({"name": name, "range": w}, "cgnat_pool_external_port_range", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "range": h}, "cgnat_pool_external_port_range", True) + + w = want.get("per_user_limit", {}).get("port") + h = have.get("per_user_limit", {}).get("port") + if w != h: + if w: + self.addcmd({"name": name, "limit": w}, "cgnat_pool_external_per_user", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "limit": h}, "cgnat_pool_external_per_user", True) + + want_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in want.get("range", [])} + have_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in have.get("range", [])} + for val, rng in want_ranges.items(): + existing = have_ranges.get(val) + if existing is None or existing != rng: + seq = rng.get("seq") if isinstance(rng, dict) else None + self.addcmd( + {"name": name, "range": val, "seq": seq}, + "cgnat_pool_external_range", + False, + ) + + if self.state in ("replaced", "overridden"): + for val in have_ranges: + if val not in want_ranges: + self.addcmd({"name": name, "range": val}, "cgnat_pool_external_range", True) + + def _compare_internal_pool(self, name, want, have): + want_ranges = set(want.get("range", [])) + have_ranges = set(have.get("range", [])) + + for rng in want_ranges - have_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", False) + + if self.state in ("replaced", "overridden"): + for rng in have_ranges - want_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", True) + + def _compare_cgnat_rules(self, wantd, haved): + want_rules = wantd.get("nat", {}).get("cgnat", {}).get("rule", {}) + have_rules = haved.get("nat", {}).get("cgnat", {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + + if self.state in ("replaced", "overridden") and w != h: + h = {} + + w_src = w.get("source", {}).get("pool") + h_src = h.get("source", {}).get("pool") + if w_src != h_src: + if w_src: + self.addcmd({"id": rid, "pool": w_src}, "cgnat_rule_source_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_src}, "cgnat_rule_source_pool", True) + + w_tr = w.get("translation", {}).get("pool") + h_tr = h.get("translation", {}).get("pool") + if w_tr != h_tr: + if w_tr: + self.addcmd({"id": rid, "pool": w_tr}, "cgnat_rule_translation_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_tr}, "cgnat_rule_translation_pool", True) + + # ------------------------------------------------------------------------- + # NAT / NAT64 / NAT66 rules + # ------------------------------------------------------------------------- + + def _compare_nat_rules(self, nat_type, section, wantd, haved): + want_rules = wantd.get(nat_type, {}).get(section, {}).get("rule", {}) + have_rules = haved.get(nat_type, {}).get(section, {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + if self.state in ("replaced", "overridden") and w != h: + h = {} + if w == h and self.state != "rendered": + continue + self._compare_rule(nat_type, section, rid, w, h) + + def _compare_rule(self, nat_type, section, rid, want, have): + ctx = {"nat": nat_type, "type": section, "id": rid} + + want_lb = want.get("load_balance") or {} + have_lb = have.get("load_balance") or {} + want_trans_addr = (want.get("translation") or {}).get("address") + have_trans_addr = (have.get("translation") or {}).get("address") + if want_lb and want_trans_addr is not None: + self._module.fail_json( + msg="translation.address and load_balance are mutually exclusive", + ) + if self.state == "merged": + if want_lb and have_trans_addr is not None: + self._module.fail_json( + msg=( + "Cannot add load_balance to a rule that already has translation.address with " + "state=merged; use state=replaced or state=overridden" + ), + ) + if want_trans_addr is not None and have_lb: + self._module.fail_json( + msg=( + "Cannot add translation.address to a rule that already has load_balance with " + "state=merged; use state=replaced or state=overridden" + ), + ) + + for field in set(want) | set(have): + if field == "inbound_interface": + continue + val = want.get(field) if field in want else have.get(field) + if isinstance(val, bool): + self._cmp_bool(want, have, field, ctx, f"nat_type_{field}") + elif isinstance(val, str): + self._cmp_scalar(want, have, field, ctx, f"nat_type_{field}") + + self._cmp_interface(want, have, ctx, nat_type, section) + self._cmp_outbound_interface(want, have, ctx) + for atype in ("destination", "source"): + self._cmp_addr_sub(want, have, atype, ctx) + self._cmp_translation(want, have, ctx) + self._cmp_match_mark(want, have, ctx) + self._cmp_nat64_pools(want, have, ctx) + self._cmp_load_balance(want, have, ctx) + + # ------------------------------------------------------------------------- + # Field-level helpers + # ------------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser): + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h: + if w: + self.addcmd(dict(ctx), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx), parser, True) + + def _cmp_interface(self, want, have, ctx, nat_type, section): + iface_w = want.get("inbound_interface") + iface_h = have.get("inbound_interface") + if iface_w == iface_h: + return + + if nat_type == "nat" and section == "static": + if iface_w: + self.addcmd(dict(ctx, value=iface_w), "nat_static_inbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h), "nat_static_inbound_interface", True) + return + + iface_w = iface_w or {} + iface_h = iface_h or {} + + if nat_type == "nat": + parser_name = "nat_inbound_interface_name" + parser_group = "nat_inbound_interface_group" + else: + parser_name = "nat6x_inbound_interface" + parser_group = "nat6x_inbound_interface" + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), parser_name, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), parser_name, True) + + if nat_type == "nat" and iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd(dict(ctx, value=iface_w["group"]), parser_group, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["group"]), parser_group, True) + + def _cmp_outbound_interface(self, want, have, ctx): + iface_w = want.get("outbound_interface") or {} + iface_h = have.get("outbound_interface") or {} + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), "nat_type_outbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), "nat_type_outbound_interface", True) + + if iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd( + dict(ctx, value=iface_w["group"]), + "nat_type_outbound_interface_group", + False, + ) + elif self.state in ("replaced", "overridden"): + self.addcmd( + dict(ctx, value=iface_h["group"]), + "nat_type_outbound_interface_group", + True, + ) + + def _cmp_addr_sub(self, want, have, atype, ctx): + sub_w = want.get(atype) or {} + sub_h = have.get(atype) or {} + if sub_w == sub_h: + return + + changed = {k: v for k, v in sub_w.items() if sub_h.get(k) != v} + removed = { + k: v + for k, v in sub_h.items() + if k not in sub_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, atype=atype, sub=changed), "nat_type_address", False) + if removed: + self.addcmd(dict(ctx, atype=atype, sub=removed), "nat_type_address", True) + + def _cmp_translation(self, want, have, ctx): + trans_w = want.get("translation") or {} + trans_h = have.get("translation") or {} + if trans_w == trans_h: + return + + changed = {k: v for k, v in trans_w.items() if k != "pool" and trans_h.get(k) != v} + removed = { + k: v + for k, v in trans_h.items() + if k != "pool" and k not in trans_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, translation=changed), "nat_type_translation_address", False) + if removed: + self.addcmd(dict(ctx, translation=removed), "nat_type_translation_address", True) + + def _cmp_match_mark(self, want, have, ctx): + w = want.get("match", {}).get("mark") + h = have.get("match", {}).get("mark") + if w != h: + if w is not None: + self.addcmd(dict(ctx, mark=w), "nat64_match_mark", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, mark=h), "nat64_match_mark", True) + + def _cmp_nat64_pools(self, want, have, ctx): + want_pools = want.get("translation", {}).get("pool", {}) + have_pools = have.get("translation", {}).get("pool", {}) + + if isinstance(want_pools, list): + want_pools = {p["id"]: p for p in want_pools} + if isinstance(have_pools, list): + have_pools = {p["id"]: p for p in have_pools} + + for pid in set(want_pools) | set(have_pools): + wp = want_pools.get(pid, {}) + hp = have_pools.get(pid, {}) + + if wp == hp: + continue + + changed = {k: v for k, v in wp.items() if k != "id" and hp.get(k) != v} + removed = { + k: v + for k, v in hp.items() + if k != "id" and k not in wp and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd( + dict(ctx, pool_id=pid, pool=changed), + "nat64_translation_pool", + False, + ) + if removed: + self.addcmd( + dict(ctx, pool_id=pid, pool=removed), + "nat64_translation_pool", + True, + ) + + def _cmp_load_balance(self, want, have, ctx): + lb_w = want.get("load_balance") or {} + lb_h = have.get("load_balance") or {} + + want_hash = set(lb_w.get("hash") or []) + have_hash = set(lb_h.get("hash") or []) + + for h in want_hash - have_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", False) + if self.state in ("replaced", "overridden"): + for h in have_hash - want_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", True) + + want_backends = lb_w.get("backend", []) + have_backends = lb_h.get("backend", []) + if isinstance(want_backends, list): + want_backends = {b["ip"]: b for b in want_backends} + if isinstance(have_backends, list): + have_backends = {b["ip"]: b for b in have_backends} + + for ip in set(want_backends) | set(have_backends): + wb = want_backends.get(ip, {}) + hb = have_backends.get(ip, {}) + if wb == hb: + continue + if wb: + weight = wb.get("weight") + if weight is None: + self._module.fail_json(msg="load_balance.backend entries require 'weight'") + self.addcmd(dict(ctx, ip=ip, weight=weight), "nat_type_lb_backend", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, ip=ip, weight=hb.get("weight")), "nat_type_lb_backend", True) diff --git a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py index 5d294063..78b7a545 100644 --- a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -18,7 +17,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -93,16 +91,16 @@ class Ntp_global(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} wantd = {} commandlist = self._commandlist(haved) servernames = self._servernames(haved) # removing the servername and commandlist from the list after deleting it from haved # iterate through the top-level items to delete - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: - for hk, hval in iteritems(have): + for hk, hval in have.items(): if hk == "allow_clients" and hk in commandlist: self.commands.append( self._tmplt.render({"": hk}, "allow_clients_delete", True), @@ -130,7 +128,7 @@ class Ntp_global(ResourceModule): commandlist = self._commandlist(haved) servernames = self._servernames(haved) - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: if "server" not in have: self._compareoverride(want={}, have=have) @@ -139,7 +137,7 @@ class Ntp_global(ResourceModule): self._compareoverride(want={}, have=have) servernames.remove(have["server"]) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -155,7 +153,7 @@ class Ntp_global(ResourceModule): def _compareoverride(self, want, have): # do not delete configuration with options level - for i, val in iteritems(have): + for i, val in have.items(): if i == "options": pass else: @@ -163,12 +161,12 @@ class Ntp_global(ResourceModule): def _ntp_list_to_dict(self, entry): servers_dict = {} - for k, data in iteritems(entry): + for k, data in entry.items(): if k == "servers": for value in data: if "options" in value: result = self._serveroptions_list_to_dict(value) - for res, resvalue in iteritems(result): + for res, resvalue in result.items(): servers_dict.update({res: resvalue}) else: servers_dict.update({value["server"]: value}) @@ -179,7 +177,7 @@ class Ntp_global(ResourceModule): def _serveroptions_list_to_dict(self, entry): serveroptions_dict = {} - for Opk, Op in iteritems(entry): + for Opk, Op in entry.items(): if Opk == "options": for val in Op: dict = {} @@ -190,16 +188,16 @@ class Ntp_global(ResourceModule): def _commandlist(self, haved): commandlist = [] - for k, have in iteritems(haved): - for ck, cval in iteritems(have): + for k, have in haved.items(): + for ck, cval in have.items(): if ck != "options" and ck not in commandlist: commandlist.append(ck) return commandlist def _servernames(self, haved): servernames = [] - for k, have in iteritems(haved): - for sk, sval in iteritems(have): + for k, have in haved.items(): + for sk, sval in have.items(): if sk != "options" and sval not in servernames: servernames.append(sval) return servernames diff --git a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py index 51b47494..bc93ac24 100644 --- a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ The vyos_ospf_interfaces config file. @@ -17,7 +16,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -27,17 +25,16 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.u 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.ospf_interfaces import ( - Ospf_interfacesTemplate + Ospf_interfacesTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( - Ospf_interfacesTemplate14 + Ospf_interfacesTemplate14, +) +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 -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Ospf_interfaces(ResourceModule): """ @@ -77,7 +74,7 @@ class Ospf_interfaces(ResourceModule): self._tmplt = Ospf_interfacesTemplate() def parse(self): - """ override parse to check template """ + """override parse to check template""" self._validate_template() return super().parse() @@ -119,12 +116,12 @@ class Ospf_interfaces(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": h_del = {} - for k, v in iteritems(haved): + for k, v in haved.items(): if k in wantd or not wantd: h_del.update({k: v}) haved = h_del have_int = [] - for k, have in iteritems(haved): + for k, have in haved.items(): if k in wantd: have_int.append(k) self._remove_ospf_int(have) @@ -132,7 +129,7 @@ class Ospf_interfaces(ResourceModule): if self.state == "overridden": have_int = [] - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: have_int.append(k) self._remove_ospf_int(have) @@ -142,17 +139,17 @@ class Ospf_interfaces(ResourceModule): # removing the interfaces from haved that are already negated for interface in have_int: haved.pop(interface) - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _remove_ospf_int(self, entry): int_name = entry.get("name", {}) int_addr = entry.get("address_family", {}) - for k, addr in iteritems(int_addr): + for k, addr in int_addr.items(): rem_entry = {"name": int_name, "address_family": {"afi": k}} self.addcmd(rem_entry, "ip_ospf", True) @@ -169,8 +166,8 @@ class Ospf_interfaces(ResourceModule): hdict = have.get("address_family", {}) wname = want.get("name") hname = have.get("name") - for name, entry in iteritems(wdict): - for key, param in iteritems(entry): + for name, entry in wdict.items(): + for key, param in entry.items(): w_addr = {"afi": name, key: param} h_addr = {} if hdict.get(name): @@ -178,8 +175,8 @@ class Ospf_interfaces(ResourceModule): w = {"name": wname, "address_family": w_addr} h = {"name": hname, "address_family": h_addr} self.compare(parsers=self.parsers, want=w, have=h) - for name, entry in iteritems(hdict): - for key, param in iteritems(entry): + for name, entry in hdict.items(): + for key, param in entry.items(): h_addr = {"afi": name, key: param} w_addr = {} w = {"name": wname, "address_family": w_addr} @@ -187,7 +184,7 @@ class Ospf_interfaces(ResourceModule): self.compare(parsers=self.parsers, want=w, have=h) def _ospf_int_list_to_dict(self, entry): - for name, family in iteritems(entry): + for name, family in entry.items(): if "address_family" in family: addr_dict = {} for entry in family.get("address_family", []): diff --git a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py index a9c1de1b..20821980 100644 --- a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -33,6 +32,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils _is_w_same, list_diff_want_only, ) +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 Ospfv2(ConfigBase): @@ -54,7 +57,7 @@ class Ospfv2(ConfigBase): :returns: The current configuration as a dictionary """ - (facts, _warnings) = Facts(self._module).get_facts( + facts, _warnings = Facts(self._module).get_facts( self.gather_subset, self.gather_network_resources, data=data, @@ -124,7 +127,7 @@ class Ospfv2(ConfigBase): want = self._module.params["config"] have = existing_ospfv2_facts - resp = self.set_state(want, have) + resp = self.set_state(remove_empties(want), remove_empties(have)) return to_list(resp) def set_state(self, w, h): @@ -136,7 +139,6 @@ class Ospfv2(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ - commands = [] if self.state in ("merged", "replaced", "overridden", "rendered") and not w: self._module.fail_json( @@ -220,7 +222,7 @@ class Ospfv2(ConfigBase): w = deepcopy(remove_empties(want)) leaf = ("default_metric", "log_adjacency_changes") if w: - for key, val in iteritems(w): + for key, val in w.items(): if opr and key in leaf and not _is_w_same(w, have, key): commands.append(self._form_attr_cmd(attr=key, val=_bool_to_str(val), opr=opr)) elif not opr and key in leaf and not _in_target(have, key): @@ -270,7 +272,6 @@ class Ospfv2(ConfigBase): :param opr: True/False. :return: generated list of commands. """ - commands = [] h = {} if have: @@ -289,7 +290,7 @@ class Ospfv2(ConfigBase): ), } leaf = leaf_dict[attr] - for item, value in iteritems(want[attr]): + for item, value in want[attr].items(): if opr and item in leaf and not _is_w_same(want[attr], h, item): if item == "enabled": item = "enable" @@ -320,7 +321,6 @@ class Ospfv2(ConfigBase): :param opr: True/False. :return: generated list of commands. """ - commands = [] h = [] if want: @@ -336,6 +336,22 @@ class Ospfv2(ConfigBase): command = cmd + attr.replace("_", "-") + " " if attr == "network": command += member["address"] + elif ( + attr == "passive_interface" + and member != "default" + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): + command = ( + command.replace("passive-interface", "interface") + member + " passive" + ) + elif attr == "passive_interface_exclude" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): + command = ( + command.replace("passive-interface-exclude", "interface") + + member + + " passive disable" + ) else: command += member commands.append(command) @@ -348,7 +364,23 @@ class Ospfv2(ConfigBase): cmd + attr.replace("_", "-") + " " + member["address"], ) elif member not in h: - commands.append(cmd + attr.replace("_", "-") + " " + member) + if ( + attr == "passive_interface" + and member != "default" + and LooseVersion(get_os_version(self._module)) + >= LooseVersion("1.4") + ): + commands.append(cmd + "interface" + " " + member + " passive") + elif attr == "passive_interface_exclude" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): + command = ( + command.replace("passive-interface-exclude", "interface") + + member + + " passive disable" + ) + else: + commands.append(cmd + attr.replace("_", "-") + " " + member) else: commands.append(cmd + " " + attr.replace("_", "-")) return commands @@ -385,7 +417,7 @@ class Ospfv2(ConfigBase): commands.append(cmd + attr.replace("_", "-")) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = self.search_obj_in_have(h, w_item, name[attr]) @@ -493,7 +525,7 @@ class Ospfv2(ConfigBase): commands.append(self._compute_command(attr=attr, opr=opr)) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = self.search_obj_in_have(h, w_item, name[attr]) @@ -590,7 +622,7 @@ class Ospfv2(ConfigBase): leaf = leaf_dict[attr] if h and key in h.keys(): h_attrib = h.get(key) or {} - for item, val in iteritems(w[key]): + for item, val in w[key].items(): if opr and item in leaf and not _is_w_same(w[key], h_attrib, item): if item in ("administrative", "always") and val: commands.append( @@ -651,7 +683,7 @@ class Ospfv2(ConfigBase): self._form_attr_cmd(key="area", attr=w_area["area_id"], opr=opr), ) else: - for key, val in iteritems(w_area): + for key, val in w_area.items(): if opr and key in l_set and not _is_w_same(w_area, h_area, key): if key == "area_id": commands.append( @@ -724,7 +756,7 @@ class Ospfv2(ConfigBase): if w_area: if h_type and key in h_type.keys(): h_area = h_type.get(key) or {} - for item, val in iteritems(w_type[key]): + for item, val in w_type[key].items(): if ( opr and item in a_type[key] diff --git a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py index 25d9a0ea..a84899f5 100644 --- a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -33,6 +32,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils _in_target, _is_w_same, ) +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 Ospfv3(ConfigBase): @@ -200,7 +203,7 @@ class Ospfv3(ConfigBase): commands = [] w = deepcopy(remove_empties(want)) if w: - for key, val in iteritems(w): + for key, val in w.items(): commands.extend(self._render_child_param(w, have, key, opr)) return commands @@ -241,7 +244,7 @@ class Ospfv3(ConfigBase): elif want[attr]: leaf_dict = {"parameters": "router_id"} leaf = leaf_dict[attr] - for item, value in iteritems(want[attr]): + for item, value in want[attr].items(): if opr and item in leaf and not _is_w_same(want[attr], h, item): commands.append(self._form_attr_cmd(key=attr, attr=item, val=value, opr=opr)) elif not opr and item in leaf and not _in_target(h, item): @@ -264,10 +267,12 @@ class Ospfv3(ConfigBase): name = { "redistribute": "route_type", "range": "address", + "interface": "name", } leaf_dict = { "redistribute": ("route_map", "route_type"), "range": ("address", "advertise", "not_advertise"), + "interface": ("name"), } leaf = leaf_dict[attr] w = want.get(attr) or [] @@ -277,19 +282,31 @@ class Ospfv3(ConfigBase): commands.append(self._compute_command(attr=attr, opr=opr)) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = search_obj_in_list(w_item[name[attr]], h, name[attr]) if opr and key in leaf and not _is_w_same(w_item, h_item, key): - if key == "route_type" or ( + if key in ["route_type", "name"] or ( key == "address" and "advertise" not in w_item and "not-advertise" not in w_item ): if not val: cmd = cmd.replace("set", "delete") - commands.append(cmd + attr + " " + str(val)) + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attr == "interface" + ): + words = cmd.split() + cmd14_list = [] + for word in words: + cmd14_list.append(word) + if word == "ospfv3": + cmd14_list.append(attr + " " + str(val)) + commands.append(" ".join(cmd14_list)) + else: + commands.append(cmd + attr + " " + str(val)) elif key in leaf_dict["range"] and key != "address": commands.append( cmd + attr + " " + w_item[name[attr]] + " " + key.replace("_", "-"), @@ -306,8 +323,20 @@ class Ospfv3(ConfigBase): + str(val), ) elif not opr and key in leaf and not _in_target(h_item, key): - if key in ("route_type", "address"): - commands.append(cmd + attr + " " + str(val)) + if key in ("route_type", "address", "name"): + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attr == "interface" + ): + words = cmd.split() + cmd14_list = [] + for word in words: + cmd14_list.append(word) + if word == "ospfv3": + cmd14_list.append(attr + " " + str(val)) + commands.append(" ".join(cmd14_list)) + else: + commands.append(cmd + attr + " " + str(val)) else: commands.append(cmd + (attr + " " + w_item[name[attr]] + " " + key)) return commands @@ -346,7 +375,7 @@ class Ospfv3(ConfigBase): self._form_attr_cmd(key="area", attr=w_area["area_id"], opr=opr), ) else: - for key, val in iteritems(w_area): + for key, val in w_area.items(): if opr and key in l_set and not _is_w_same(w_area, h_area, key): if key == "area_id": commands.append( @@ -373,6 +402,10 @@ class Ospfv3(ConfigBase): commands.extend( self._render_list_dict_param(key, w_area, h_area, cmd, opr), ) + elif key == "interface": + commands.extend( + self._render_list_dict_param(key, w_area, h_area, cmd, opr), + ) return commands def _form_attr_cmd(self, key=None, attr=None, val=None, opr=True): diff --git a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py index 9da27c15..05164c31 100644 --- a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -19,7 +18,6 @@ created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -86,23 +84,23 @@ class Prefix_lists(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} - for key, hvalue in iteritems(haved): + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} + for key, hvalue in haved.items(): wvalue = wantd.pop(key, {}) if wvalue: wplists = wvalue.get("prefix_lists", {}) hplists = hvalue.get("prefix_lists", {}) hvalue["prefix_lists"] = { - k: v for k, v in iteritems(hplists) if k in wplists or not wplists + k: v for k, v in hplists.items() if k in wplists or not wplists } # remove superfluous config for overridden and deleted if self.state in ["overridden", "deleted"]: - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -127,7 +125,7 @@ class Prefix_lists(ResourceModule): ) def _compare_plists(self, want, have): - for wk, wentry in iteritems(want): + for wk, wentry in want.items(): hentry = have.pop(wk, {}) # parser list for name and descriptions @@ -143,7 +141,7 @@ class Prefix_lists(ResourceModule): self._compare_rules(want=wplrules, have=hplrules) def _compare_rules(self, want, have): - for wr, wrule in iteritems(want): + for wr, wrule in want.items(): hrule = have.pop(wr, {}) # parser list for entries @@ -164,7 +162,7 @@ class Prefix_lists(ResourceModule): ) def _prefix_list_list_to_dict(self, entry): - for afi, value in iteritems(entry): + for afi, value in entry.items(): if "prefix_lists" in value: for pl in value["prefix_lists"]: pl.update({"afi": afi}) diff --git a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py index 9b6c3e9d..948341d3 100644 --- a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -19,7 +18,6 @@ created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -31,6 +29,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps import ( Route_mapsTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +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 Route_maps(ResourceModule): @@ -59,6 +64,8 @@ class Route_maps(ResourceModule): "set_bgp_extcommunity_rt", "set_extcommunity_rt", "set_extcommunity_soo", + "set_extcommunity_bandwidth", + "set_extcommunity_bandwidth_non_transitive", "set_ip_next_hop", "set_ipv6_next_hop", "set_large_community", @@ -70,6 +77,7 @@ class Route_maps(ResourceModule): "set_src", "set_tag", "set_weight", + "set_table", "set_comm_list", "set_comm_list_delete", "set_community", @@ -89,15 +97,34 @@ class Route_maps(ResourceModule): "on_match_next", "match_ipv6_address", "match_ipv6_nexthop", + "match_protocol", "match_rpki", ] + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = Route_mapsTemplate14() + else: + self._tmplt = Route_mapsTemplate() + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) + def execute_module(self): """Execute the module :rtype: A dictionary :returns: The result from module execution """ + self._validate_template() if self.state not in ["parsed", "gathered"]: self.generate_commands() self.run_commands() @@ -116,16 +143,16 @@ class Route_maps(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} wantd = {} # remove superfluous config for overridden and deleted if self.state in ["overridden", "deleted"]: - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self.commands.append(self._tmplt.render({"route_map": k}, "route_map", True)) - for wk, want in iteritems(wantd): + for wk, want in wantd.items(): self._compare(want=want, have=haved.pop(wk, {})) def _compare(self, want, have): @@ -139,13 +166,13 @@ class Route_maps(ResourceModule): self._compare_entries(want=w_entries, have=h_entries) def _compare_entries(self, want, have): - for wk, wentry in iteritems(want): + for wk, wentry in want.items(): hentry = have.pop(wk, {}) self.compare(parsers=self.parsers, want=wentry, have=hentry) def _route_maps_list_to_dict(self, entry): entry = {x["route_map"]: x for x in entry} - for rmap, data in iteritems(entry): + for rmap, data in entry.items(): if "entries" in data: for x in data["entries"]: x.update({"route_map": rmap}) diff --git a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py index 9497d7fa..cdc1d6e1 100644 --- a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -107,13 +105,13 @@ class Snmp_server(ResourceModule): """ self._compare_lists(want, have) self._compare_snmp_v3(want, have) - for key, value in iteritems(want): + for key, value in want.items(): self.compare( parsers=self.parsers, want={key: value}, have={key: have.pop(key, "")}, ) - for key, entry in iteritems(have): + for key, entry in have.items(): if entry: self.compare(parsers=self.parsers, want={}, have={key: entry}) @@ -125,10 +123,10 @@ class Snmp_server(ResourceModule): for attrib in parsers: wdict = get_from_dict(want, attrib) or {} hdict = get_from_dict(have, attrib) or {} - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): # self.addcmd(entry, attrib, False) if attrib == "communities": - for k, v in iteritems(entry): + for k, v in entry.items(): if k in ["clients", "networks"]: v.sort() h = {} @@ -153,9 +151,9 @@ class Snmp_server(ResourceModule): ) have.pop(attrib, {}) # remove remaining items in have for replaced - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): if attrib == "communities": - for k, v in iteritems(entry): + for k, v in entry.items(): if k != "name": self.compare( parsers="communities", @@ -188,9 +186,9 @@ class Snmp_server(ResourceModule): for attrib in attribute_dict.keys(): wattrib = get_from_dict(wdict, attrib) or {} hattrib = get_from_dict(hdict, attrib) or {} - for key, entry in iteritems(wattrib): + for key, entry in wattrib.items(): self._compare_snmp_v3_auth_privacy(entry, hattrib.get(key, {}), attrib) - for k, v in iteritems(entry): + for k, v in entry.items(): if k != attribute_dict[attrib]: h = {} if hattrib.get(key): @@ -217,11 +215,11 @@ class Snmp_server(ResourceModule): have=h, ) # remove remaining items in have for replaced - for key, entry in iteritems(hattrib): + for key, entry in hattrib.items(): self._compare_snmp_v3_auth_privacy({}, entry, attrib) self.compare(parsers=parsers, want={}, have={"snmp_v3": {attrib: entry}}) hdict.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): # self.addcmd(entry, attrib, False) self.compare( parsers="snmp_v3.engine_id", @@ -229,7 +227,7 @@ class Snmp_server(ResourceModule): have={"snmp_v3": {key: hdict.pop(key, {})}}, ) # remove remaining items in have for replaced - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): self.compare(parsers=parsers, want={}, have={"snmp_v3": {key: entry}}) def _compare_snmp_v3_auth_privacy(self, wattrib, hattrib, attrib): @@ -244,7 +242,7 @@ class Snmp_server(ResourceModule): primary_key = "user" else: primary_key = "address" - for key, entry in iteritems(wattrib): + for key, entry in wattrib.items(): if key != primary_key and key in ["authentication", "privacy"]: self.compare( parsers=parsers, @@ -265,7 +263,7 @@ class Snmp_server(ResourceModule): }, }, ) - for key, entry in iteritems(hattrib): + for key, entry in hattrib.items(): if key != primary_key and key in ["authentication", "privacy"]: self.compare( parsers=parsers, @@ -291,13 +289,13 @@ class Snmp_server(ResourceModule): "views": "view", "trap_targets": "address", } - for k, v in iteritems(param_dict): + for k, v in param_dict.items(): if k in entry: a_dict = {} for el in entry[k]: a_dict.update({el[v]: el}) entry[k] = a_dict - for k, v in iteritems(v3_param_dict): + for k, v in v3_param_dict.items(): if entry.get("snmp_v3") and k in entry.get("snmp_v3"): a_dict = {} for el in entry["snmp_v3"][k]: diff --git a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py index 8451e7da..2a09c0f0 100644 --- a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py @@ -13,11 +13,9 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -182,7 +180,7 @@ class Static_routes(ConfigBase): """ commands = [] if have: - for key, value in iteritems(want): + for key, value in want.items(): if value: if key == "next_hops": commands.extend(self._update_next_hop(want, have)) @@ -257,7 +255,7 @@ class Static_routes(ConfigBase): """ commands = [] have = {} - for key, value in iteritems(want): + for key, value in want.items(): if value: if key == "dest": commands.append(self._compute_command(dest=want["dest"])) @@ -287,7 +285,7 @@ class Static_routes(ConfigBase): updates = dict_delete(want_blackhole, have_blackhole) if updates: - for attrib, value in iteritems(updates): + for attrib, value in updates.items(): if value: if attrib == "distance": commands.append( @@ -394,7 +392,7 @@ class Static_routes(ConfigBase): have_blackhole = have_copy.get(key) or {} updates = dict_delete(have_blackhole, want_blackhole) if updates: - for attrib, value in iteritems(updates): + for attrib, value in updates.items(): if value: if attrib == "distance": commands.append( @@ -508,7 +506,7 @@ class Static_routes(ConfigBase): commands.extend(self._add_next_hop(want, have, opr=opr)) if opr and updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value: if key == "blackhole_config": commands.extend(self._add_blackhole(key, want, have)) diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..2d986cb4 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,665 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec 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. + +Follows the established per-module convention used by vyos_ha/vyos_nat +(list-to-dict conversion + explicit per-state branching in +generate_commands), rather than a shared generic engine. + +State semantics (standard Ansible RM convention, confirmed against a +real device run that caught a bug in an earlier version of this file): + - merged: only items/fields named in `want` are touched. Nothing + absent from `want` is ever deleted. + - replaced: only items NAMED in `want` are touched (same item scope + as merged) -- but for each named item, its full state is + reconciled to exactly match `want` (fields present in + `have` but omitted from `want` ARE deleted). Items not + named in `want` at all are left completely alone. + - overridden: every item is in scope, including ones absent from + `want` entirely -- those get deleted wholesale. Named + items are reconciled the same way as `replaced`. + +This is implemented via two independent flags: + - select_all: whether item iteration considers have-only items too + (True only for overridden; False for merged/replaced). + - reconcile: whether omitted fields within an already-selected item + get deleted (True for replaced/overridden; False for + merged/rendered). +""" + +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 ( + dict_merge, +) + +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.vpn_ipsec import ( + Vpn_ipsecTemplate, +) + + +class Vpn_ipsec(ResourceModule): + """ + The vyos_vpn_ipsec config class + """ + + def __init__(self, module): + super(Vpn_ipsec, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vpn_ipsec", + tmplt=Vpn_ipsecTemplate(), + ) + self.parsers = [ + "esp_group", + "esp_group.proposal", + "esp_group.proposal.encryption", + "esp_group.proposal.hash", + "esp_group.compression", + "esp_group.disable_rekey", + "esp_group.life_bytes", + "esp_group.life_packets", + "esp_group.lifetime", + "esp_group.mode", + "esp_group.pfs", + "ike_group", + "ike_group.key_exchange", + "ike_group.proposal", + "ike_group.proposal.dh_group", + "ike_group.proposal.encryption", + "ike_group.proposal.hash", + "ike_group.close_action", + "ike_group.dead_peer_detection.action", + "ike_group.dead_peer_detection.interval", + "ike_group.dead_peer_detection.timeout", + "ike_group.disable_mobike", + "ike_group.ikev2_reauth", + "ike_group.lifetime", + "ike_group.mode", + "profile", + "profile.authentication.mode", + "profile.authentication.pre_shared_secret", + "profile.esp_group", + "profile.ike_group", + "profile.disable", + "authentication.psk.secret_type", + "authentication.psk.dhcp_interface", + "authentication.ppk", + "authentication.ppk.id", + "authentication.ppk.secret", + "authentication.ppk.secret_type", + "interface", + "log.level", + "log.subsystem", + "options.disable_route_autoinstall", + "options.flexvpn", + "options.interface", + "options.retransmission.attempts", + "options.retransmission.base", + "options.retransmission.timeout", + "options.virtual_ip", + "disable_uniqreqids", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = deepcopy(self.want) or {} + haved = deepcopy(self.have) or {} + + for entry in (wantd, haved): + self._list_to_dict(entry) + + scoped_delete = None + if self.state == "deleted": + if wantd: + # user named specific items -- surgical removal of just + # those, everything else preserved (vyos_vrf precedent: + # deleted + instances:[{name: vrf-blue}] removes only + # vrf-blue). Capture what was named before wiping wantd. + scoped_delete = wantd + wantd = {} + + if self.state == "merged": + # NOTE: list_to_dict() above must run BEFORE this. dict_merge + # concatenates lists rather than merging matching entries by + # key, so merging while ike_group/esp_group/etc are still + # lists would duplicate entries instead of filling in omitted + # fields from `have`. Once they're name-keyed dicts, dict_merge + # recurses per-key correctly, which is what lets a partial + # update (e.g. specifying only key_exchange) leave other + # existing fields on that same group untouched. + wantd = dict_merge(haved, wantd) + + select_all = self.state in ("overridden", "deleted") + reconcile = self.state in ("replaced", "overridden", "deleted") + + self._compare_esp_groups(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_ike_groups(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_profiles(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_psks(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_ppks(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_top_level(wantd, haved, select_all, reconcile, scoped_delete) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------- + # List -> name-keyed dict conversion (matches vyos_ha/vyos_nat style) + # ------------------------------------------------------------------- + + def _list_to_dict(self, config): + for key in ("ike_group", "esp_group", "profile"): + items = config.get(key) + if isinstance(items, list): + config[key] = {item["name"]: item for item in items} + for item in config[key].values(): + if isinstance(item.get("proposal"), list): + item["proposal"] = {p["proposal_id"]: p for p in item["proposal"]} + + auth = config.get("authentication", {}) + for key in ("psk", "ppk"): + items = auth.get(key) + if isinstance(items, list): + auth[key] = {item["name"]: item for item in items} + + # ------------------------------------------------------------------- + # ESP groups + # ------------------------------------------------------------------- + + def _compare_esp_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_groups = haved.get("esp_group", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("esp_group", {})): + if name in have_groups: + self.commands.append("delete vpn ipsec esp-group {0}".format(name)) + return + + want_groups = wantd.get("esp_group", {}) + names = set(want_groups) | set(have_groups) if select_all else set(want_groups) + + for name in names: + w = want_groups.get(name, {}) + h = have_groups.get(name, {}) + if w == h: + continue + + if name in have_groups and name not in want_groups: + # only reached when select_all (overridden): item entirely + # absent from want -> delete wholesale + self.commands.append("delete vpn ipsec esp-group {0}".format(name)) + continue + + if name not in have_groups: + self.addcmd({"name": name}, "esp_group", False) + + for field in ("mode", "pfs", "lifetime", "life_bytes", "life_packets"): + self._cmp_scalar( + w, + h, + field, + {"name": name}, + "esp_group.{0}".format(field), + reconcile, + ) + for field in ("compression", "disable_rekey"): + self._cmp_bool( + w, + h, + field, + {"name": name}, + "esp_group.{0}".format(field), + reconcile, + ) + + self._compare_proposals( + w.get("proposal", {}), + h.get("proposal", {}), + {"name": name}, + "esp_group.proposal", + "esp_group.proposal.encryption", + "esp_group.proposal.hash", + None, + reconcile, + ) + + # ------------------------------------------------------------------- + # IKE groups + # ------------------------------------------------------------------- + + def _compare_ike_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_groups = haved.get("ike_group", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("ike_group", {})): + if name in have_groups: + self.commands.append("delete vpn ipsec ike-group {0}".format(name)) + return + + want_groups = wantd.get("ike_group", {}) + names = set(want_groups) | set(have_groups) if select_all else set(want_groups) + + for name in names: + w = want_groups.get(name, {}) + h = have_groups.get(name, {}) + if w == h: + continue + + if name in have_groups and name not in want_groups: + self.commands.append("delete vpn ipsec ike-group {0}".format(name)) + continue + + if name not in have_groups: + self.addcmd({"name": name}, "ike_group", False) + + self._cmp_scalar( + w, + h, + "key_exchange", + {"name": name}, + "ike_group.key_exchange", + reconcile, + ) + for field in ("close_action", "lifetime", "mode"): + self._cmp_scalar( + w, + h, + field, + {"name": name}, + "ike_group.{0}".format(field), + reconcile, + ) + for field in ("disable_mobike", "ikev2_reauth"): + self._cmp_bool( + w, + h, + field, + {"name": name}, + "ike_group.{0}".format(field), + reconcile, + ) + + w_dpd = w.get("dead_peer_detection", {}) + h_dpd = h.get("dead_peer_detection", {}) + for field in ("action", "interval", "timeout"): + self._cmp_scalar( + w_dpd, + h_dpd, + field, + {"name": name}, + "ike_group.dead_peer_detection.{0}".format(field), + reconcile, + ) + + self._compare_proposals( + w.get("proposal", {}), + h.get("proposal", {}), + {"name": name}, + "ike_group.proposal", + "ike_group.proposal.encryption", + "ike_group.proposal.hash", + "ike_group.proposal.dh_group", + reconcile, + ) + + # ------------------------------------------------------------------- + # Proposals (shared by esp_group / ike_group) + # ------------------------------------------------------------------- + + def _compare_proposals( + self, + want_props, + have_props, + group_ctx, + bare_parser, + encryption_parser, + hash_parser, + dh_group_parser, + reconcile, + ): + # a proposal collection lives entirely inside an already-selected + # group -- once that group is in scope, its own proposals always + # get full reconciliation under replaced/overridden (never a + # separate select_all concern of their own). + ids = set(want_props) | set(have_props) if reconcile else set(want_props) + for pid in ids: + w = want_props.get(pid, {}) + h = have_props.get(pid, {}) + if w == h: + continue + + if pid in have_props and pid not in want_props: + self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, True) + continue + + if pid not in have_props: + self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, False) + + ctx = dict(group_ctx, proposal_id=pid) + self._cmp_scalar(w, h, "encryption", ctx, encryption_parser, reconcile) + self._cmp_scalar(w, h, "hash", ctx, hash_parser, reconcile) + if dh_group_parser: + self._cmp_scalar(w, h, "dh_group", ctx, dh_group_parser, reconcile) + + # ------------------------------------------------------------------- + # Profiles + # ------------------------------------------------------------------- + + def _compare_profiles(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_profiles = haved.get("profile", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("profile", {})): + if name in have_profiles: + self.commands.append("delete vpn ipsec profile {0}".format(name)) + return + + want_profiles = wantd.get("profile", {}) + names = set(want_profiles) | set(have_profiles) if select_all else set(want_profiles) + + for name in names: + w = want_profiles.get(name, {}) + h = have_profiles.get(name, {}) + if w == h: + continue + + if name in have_profiles and name not in want_profiles: + self.commands.append("delete vpn ipsec profile {0}".format(name)) + continue + + if name not in have_profiles: + self.addcmd({"name": name}, "profile", False) + + ctx = {"name": name} + w_auth = w.get("authentication", {}) + h_auth = h.get("authentication", {}) + self._cmp_scalar( + w_auth, + h_auth, + "mode", + ctx, + "profile.authentication.mode", + reconcile, + ) + self._cmp_scalar( + w_auth, + h_auth, + "pre_shared_secret", + ctx, + "profile.authentication.pre_shared_secret", + reconcile, + ) + self._cmp_scalar(w, h, "esp_group", ctx, "profile.esp_group", reconcile) + self._cmp_scalar(w, h, "ike_group", ctx, "profile.ike_group", reconcile) + self._cmp_bool(w, h, "disable", ctx, "profile.disable", reconcile) + + w_tunnels = set(w.get("bind_tunnel") or []) + h_tunnels = set(h.get("bind_tunnel") or []) + for tun in w_tunnels - h_tunnels: + self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", False) + if reconcile: + for tun in h_tunnels - w_tunnels: + self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", True) + + # ------------------------------------------------------------------- + # PSKs + # ------------------------------------------------------------------- + + def _compare_psks(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_psks = haved.get("authentication", {}).get("psk", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("authentication", {}).get("psk", {})): + if name in have_psks: + self.commands.append( + "delete vpn ipsec authentication psk {0}".format(name), + ) + return + + want_psks = wantd.get("authentication", {}).get("psk", {}) + names = set(want_psks) | set(have_psks) if select_all else set(want_psks) + + for name in names: + w = want_psks.get(name, {}) + h = have_psks.get(name, {}) + if w == h: + continue + + if name in have_psks and name not in want_psks: + self.commands.append("delete vpn ipsec authentication psk {0}".format(name)) + continue + + if name not in have_psks: + self.addcmd({"name": name}, "authentication.psk", False) + + ctx = {"name": name} + self._cmp_scalar(w, h, "secret", ctx, "authentication.psk.secret", reconcile) + self._cmp_scalar( + w, + h, + "secret_type", + ctx, + "authentication.psk.secret_type", + reconcile, + ) + + w_ids = set(w.get("id") or []) + h_ids = set(h.get("id") or []) + for i in w_ids - h_ids: + self.addcmd(dict(ctx, id=i), "authentication.psk.id", False) + if reconcile: + for i in h_ids - w_ids: + self.addcmd(dict(ctx, id=i), "authentication.psk.id", True) + + w_dhcp = set(w.get("dhcp_interface") or []) + h_dhcp = set(h.get("dhcp_interface") or []) + for i in w_dhcp - h_dhcp: + self.addcmd(dict(ctx, dhcp_interface=i), "authentication.psk.dhcp_interface", False) + if reconcile: + for i in h_dhcp - w_dhcp: + self.addcmd( + dict(ctx, dhcp_interface=i), + "authentication.psk.dhcp_interface", + True, + ) + + def _compare_ppks(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_ppks = haved.get("authentication", {}).get("ppk", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("authentication", {}).get("ppk", {})): + if name in have_ppks: + self.commands.append( + "delete vpn ipsec authentication ppk {0}".format(name), + ) + return + + want_ppks = wantd.get("authentication", {}).get("ppk", {}) + names = set(want_ppks) | set(have_ppks) if select_all else set(want_ppks) + + for name in names: + w = want_ppks.get(name, {}) + h = have_ppks.get(name, {}) + if w == h: + continue + + if name in have_ppks and name not in want_ppks: + self.commands.append("delete vpn ipsec authentication ppk {0}".format(name)) + continue + + if name not in have_ppks: + self.addcmd({"name": name}, "authentication.ppk", False) + + ctx = {"name": name} + self._cmp_scalar(w, h, "secret", ctx, "authentication.ppk.secret", reconcile) + self._cmp_scalar( + w, + h, + "secret_type", + ctx, + "authentication.ppk.secret_type", + reconcile, + ) + + w_ids = set(w.get("id") or []) + h_ids = set(h.get("id") or []) + for i in w_ids - h_ids: + self.addcmd(dict(ctx, id=i), "authentication.ppk.id", False) + if reconcile: + for i in h_ids - w_ids: + self.addcmd(dict(ctx, id=i), "authentication.ppk.id", True) + + # ------------------------------------------------------------------- + # Top-level scalar / list / bool fields + # + # NOTE: these are all direct fields of the single top-level config + # object, not named collections -- there is no "item entirely absent + # from want" concept here, only "field omitted from want". So only + # `reconcile` applies; `select_all` is irrelevant at this level (it's + # accepted for a consistent call signature but unused). + # ------------------------------------------------------------------- + + def _compare_top_level(self, wantd, haved, select_all, reconcile, scoped_delete=None): + if scoped_delete is not None: + # Principle: naming a parameter under scoped `deleted` means + # "delete this specific value" -- a scalar/bool key present + # (regardless of value) signals whole-field removal; a list + # value present means "delete exactly these elements", not + # the whole list, mirroring vyos_vrf's bind_to_all precedent + # extended consistently to list- and nested-dict-shaped + # fields. + if "disable_uniqreqids" in scoped_delete and haved.get("disable_uniqreqids"): + self.commands.append("delete vpn ipsec disable-uniqreqids") + + h_ifaces = set(haved.get("interface") or []) + for i in set(scoped_delete.get("interface") or []) & h_ifaces: + self.addcmd({"interface": i}, "interface", True) + + s_log = scoped_delete.get("log", {}) + h_log = haved.get("log", {}) + if "level" in s_log and "level" in h_log: + self.addcmd({"level": h_log["level"]}, "log.level", True) + h_sub = set(h_log.get("subsystem") or []) + for s in set(s_log.get("subsystem") or []) & h_sub: + self.addcmd({"subsystem": s}, "log.subsystem", True) + + s_opt = scoped_delete.get("options", {}) + h_opt = haved.get("options", {}) + for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"): + if field in s_opt and h_opt.get(field): + self.addcmd({}, "options.{0}".format(field), True) + if "interface" in s_opt and "interface" in h_opt: + self.addcmd({"interface": h_opt["interface"]}, "options.interface", True) + + s_retrans = s_opt.get("retransmission", {}) + h_retrans = h_opt.get("retransmission", {}) + for field in ("attempts", "base", "timeout"): + if field in s_retrans and field in h_retrans: + self.addcmd( + {field: h_retrans[field]}, + "options.retransmission.{0}".format(field), + True, + ) + return + + self._cmp_bool(wantd, haved, "disable_uniqreqids", {}, "disable_uniqreqids", reconcile) + + w_ifaces = set(wantd.get("interface") or []) + h_ifaces = set(haved.get("interface") or []) + for i in w_ifaces - h_ifaces: + self.addcmd({"interface": i}, "interface", False) + if reconcile: + for i in h_ifaces - w_ifaces: + self.addcmd({"interface": i}, "interface", True) + + w_log = wantd.get("log", {}) + h_log = haved.get("log", {}) + self._cmp_scalar(w_log, h_log, "level", {}, "log.level", reconcile) + w_sub = set(w_log.get("subsystem") or []) + h_sub = set(h_log.get("subsystem") or []) + for s in w_sub - h_sub: + self.addcmd({"subsystem": s}, "log.subsystem", False) + if reconcile: + for s in h_sub - w_sub: + self.addcmd({"subsystem": s}, "log.subsystem", True) + + w_opt = wantd.get("options", {}) + h_opt = haved.get("options", {}) + for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"): + self._cmp_bool(w_opt, h_opt, field, {}, "options.{0}".format(field), reconcile) + self._cmp_scalar(w_opt, h_opt, "interface", {}, "options.interface", reconcile) + + w_retrans = w_opt.get("retransmission", {}) + h_retrans = h_opt.get("retransmission", {}) + for field in ("attempts", "base", "timeout"): + self._cmp_scalar( + w_retrans, + h_retrans, + field, + {}, + "options.retransmission.{0}".format(field), + reconcile, + ) + + # ------------------------------------------------------------------- + # Field-level helpers (mirrors vyos_nat's _cmp_scalar / _cmp_bool) + # ------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif reconcile and h is not None: + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False): + # An explicitly-specified value (even False) is always enforced, + # regardless of state -- that's the user directly saying what + # they want. An OMITTED field is only enforced (i.e. deleted if + # currently True) under full reconciliation (replaced/overridden). + # Under merged, an omitted field is left alone -- protected + # further upstream by dict_merge backfilling `want` from `have` + # before this is ever reached, but this still needs to be correct + # in isolation (e.g. for a field nested inside a dict that wasn't + # part of the dict_merge'd top-level structure). + explicit = field in want + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h and (w or explicit or reconcile): + self.addcmd(dict(ctx), parser, not w) diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..65232ed7 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,355 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec_s2s 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. + +Mirrors vyos_vpn_ipsec's config.py exactly -- same list-to-dict +conversion + explicit per-state branching, same select_all/reconcile +two-flag design for the replaced/overridden distinction, same scoped +deleted handling. See that file's own docstring for the full state +semantics; the summary: + + - merged: only items/fields named in `want` are touched. + - replaced: only items NAMED in `want` are touched, but each named + item is fully reconciled (omitted fields removed). + - overridden: every item is in scope, including ones absent from + `want` -- those get deleted wholesale. Named items + reconciled the same way as replaced. + - deleted: bare (no config) deletes everything; a scoped config + deletes only what's named, down to individual list + elements. +""" + +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 ( + dict_merge, +) + +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.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2s(ResourceModule): + """ + The vyos_vpn_ipsec_s2s config class + """ + + def __init__(self, module): + super(Vpn_ipsec_s2s, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vpn_ipsec_s2s", + tmplt=Vpn_ipsec_s2sTemplate(), + ) + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) or {} + haved = deepcopy(self.have) or {} + + for entry in (wantd, haved): + self._list_to_dict(entry) + + scoped_delete = None + if self.state == "deleted": + if wantd: + scoped_delete = wantd + wantd = {} + + if self.state == "merged": + # list_to_dict() above must run BEFORE this -- dict_merge + # concatenates raw lists rather than merging matching items + # by key, so it only does the right thing once both sides + # are already name-keyed dicts. + wantd = dict_merge(haved, wantd) + + select_all = self.state in ("overridden", "deleted") + reconcile = self.state in ("replaced", "overridden", "deleted") + + self._compare_peers(wantd, haved, select_all, reconcile, scoped_delete) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------- + # List -> name-keyed dict conversion + # ------------------------------------------------------------------- + + def _list_to_dict(self, config): + peers = config.get("peer") + if isinstance(peers, list): + config["peer"] = {p["name"]: p for p in peers} + for peer in config["peer"].values(): + if isinstance(peer.get("tunnel"), list): + peer["tunnel"] = {t["tunnel_id"]: t for t in peer["tunnel"]} + + # ------------------------------------------------------------------- + # Peers + # ------------------------------------------------------------------- + + def _compare_peers(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_peers = haved.get("peer", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("peer", {})): + if name in have_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + return + + want_peers = wantd.get("peer", {}) + names = set(want_peers) | set(have_peers) if select_all else set(want_peers) + + for name in names: + w = want_peers.get(name, {}) + h = have_peers.get(name, {}) + if w == h: + continue + + if name in have_peers and name not in want_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + continue + + if name not in have_peers: + self.addcmd({"name": name}, "peer", False) + + ctx = {"name": name} + self._cmp_bool(w, h, "disable", ctx, "peer.disable", reconcile) + + w_auth = w.get("authentication", {}) + h_auth = h.get("authentication", {}) + for field in ("local_id", "remote_id", "mode"): + self._cmp_scalar( + w_auth, + h_auth, + field, + ctx, + "peer.authentication.{0}".format(field), + reconcile, + ) + self._cmp_bool( + w_auth, + h_auth, + "use_x509_id", + ctx, + "peer.authentication.use_x509_id", + reconcile, + ) + + w_ppk = w_auth.get("ppk", {}) + h_ppk = h_auth.get("ppk", {}) + self._cmp_scalar(w_ppk, h_ppk, "id", ctx, "peer.authentication.ppk.id", reconcile) + self._cmp_bool( + w_ppk, + h_ppk, + "required", + ctx, + "peer.authentication.ppk.required", + reconcile, + ) + + w_rsa = w_auth.get("rsa", {}) + h_rsa = h_auth.get("rsa", {}) + for field in ("local_key", "remote_key", "passphrase"): + self._cmp_scalar( + w_rsa, + h_rsa, + field, + ctx, + "peer.authentication.rsa.{0}".format(field), + reconcile, + ) + + w_x509 = w_auth.get("x509", {}) + h_x509 = h_auth.get("x509", {}) + for field in ("certificate", "passphrase"): + self._cmp_scalar( + w_x509, + h_x509, + field, + ctx, + "peer.authentication.x509.{0}".format(field), + reconcile, + ) + w_ca = set(w_x509.get("ca_certificate") or []) + h_ca = set(h_x509.get("ca_certificate") or []) + for cert in w_ca - h_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + False, + ) + if reconcile: + for cert in h_ca - w_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + True, + ) + + for field in ( + "childless", + "connection_type", + "default_esp_group", + "description", + "dhcp_interface", + "ike_group", + "ikev2_reauth", + "local_address", + ): + self._cmp_scalar(w, h, field, ctx, "peer.{0}".format(field), reconcile) + self._cmp_bool( + w, + h, + "force_udp_encapsulation", + ctx, + "peer.force_udp_encapsulation", + reconcile, + ) + self._cmp_scalar(w, h, "replay_window", ctx, "peer.replay_window", reconcile) + + w_remote_addr = set(w.get("remote_address") or []) + h_remote_addr = set(h.get("remote_address") or []) + for addr in w_remote_addr - h_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", False) + if reconcile: + for addr in h_remote_addr - w_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", True) + + w_virt_addr = set(w.get("virtual_address") or []) + h_virt_addr = set(h.get("virtual_address") or []) + for addr in w_virt_addr - h_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", False) + if reconcile: + for addr in h_virt_addr - w_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", True) + + self._compare_tunnels(w.get("tunnel", {}), h.get("tunnel", {}), ctx, reconcile) + self._compare_vti(w.get("vti", {}), h.get("vti", {}), ctx, reconcile) + + # ------------------------------------------------------------------- + # Tunnels (nested collection within a peer) + # ------------------------------------------------------------------- + + def _compare_tunnels(self, want_tunnels, have_tunnels, peer_ctx, reconcile): + # A tunnel collection lives entirely inside an already-selected + # peer -- once that peer is in scope, its own tunnels always get + # full reconciliation under replaced/overridden, matching how + # esp_group/ike_group's own nested proposals behave in the + # profile module. + ids = set(want_tunnels) | set(have_tunnels) if reconcile else set(want_tunnels) + for tid in ids: + w = want_tunnels.get(tid, {}) + h = have_tunnels.get(tid, {}) + if w == h: + continue + + if tid in have_tunnels and tid not in want_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", True) + continue + + if tid not in have_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", False) + + ctx = dict(peer_ctx, tunnel_id=tid) + self._cmp_bool(w, h, "disable", ctx, "peer.tunnel.disable", reconcile) + for field in ("esp_group", "protocol"): + self._cmp_scalar(w, h, field, ctx, "peer.tunnel.{0}".format(field), reconcile) + self._cmp_scalar(w, h, "priority", ctx, "peer.tunnel.priority", reconcile) + + for side in ("local", "remote"): + w_side = w.get(side, {}) + h_side = h.get(side, {}) + self._cmp_scalar( + w_side, + h_side, + "port", + ctx, + "peer.tunnel.{0}.port".format(side), + reconcile, + ) + w_prefix = set(w_side.get("prefix") or []) + h_prefix = set(h_side.get("prefix") or []) + for p in w_prefix - h_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + False, + ) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + True, + ) + + # ------------------------------------------------------------------- + # VTI (nested dict within a peer, not a collection) + # ------------------------------------------------------------------- + + def _compare_vti(self, w_vti, h_vti, peer_ctx, reconcile): + for field in ("bind", "esp_group"): + self._cmp_scalar(w_vti, h_vti, field, peer_ctx, "peer.vti.{0}".format(field), reconcile) + + w_ts = w_vti.get("traffic_selector", {}) + h_ts = h_vti.get("traffic_selector", {}) + for side in ("local", "remote"): + w_prefix = set(w_ts.get(side, {}).get("prefix") or []) + h_prefix = set(h_ts.get(side, {}).get("prefix") or []) + parser = "peer.vti.traffic_selector.{0}.prefix".format(side) + for p in w_prefix - h_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, False) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, True) + + # ------------------------------------------------------------------- + # Field-level helpers (mirrors vyos_vpn_ipsec's own) + # ------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif reconcile and h is not None: + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False): + explicit = field in want + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h and (w or explicit or reconcile): + self.addcmd(dict(ctx), parser, not w) diff --git a/plugins/module_utils/network/vyos/config/vrf/__init__.py b/plugins/module_utils/network/vyos/config/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.py b/plugins/module_utils/network/vyos/config/vrf/vrf.py new file mode 100644 index 00000000..a55073b0 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vrf/vrf.py @@ -0,0 +1,307 @@ +# +# -*- 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_vrf 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.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import ( + Bgp_global, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv2.ospfv2 import ( + Ospfv2, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv3.ospfv3 import ( + Ospfv3, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.static_routes.static_routes import ( + Static_routes, +) +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.vrf import ( + VrfTemplate, +) +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 Vrf(ResourceModule): + """ + The vyos_vrf config class + """ + + def __init__(self, module): + super(Vrf, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vrf", + tmplt=VrfTemplate(), + ) + self.parsers = [ + "bind_to_all", + ] + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = VrfTemplate() + else: + self._module.fail_json(msg="VRF is not supported in this version of VyOS") + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + + # if state is merged, merge want onto have and then compare + if self.state in ["merged", "replaced"]: + + wantd = combine(haved, wantd, recursive=True) + + # if state is deleted, delete and empty out wantd + if self.state == "deleted": + w = deepcopy(wantd) + if w == {} and haved != {}: + self.commands = ["delete vrf"] + return + for k, want in w.items(): + if not (k in haved and haved[k]): + del wantd[k] + else: + if isinstance(want, list): + for entry in want: + wname = entry.get("name") + haved["instances"] = [ + i for i in haved.get("instances", []) if i.get("name") != wname + ] + self.commands.append("delete vrf name {}".format(wname)) + else: + self.commands.append("delete vrf {}".format(k.replace("_", "-"))) + del wantd[k] + + if self.state == "overridden": + w = deepcopy(wantd) + h = deepcopy(haved) + for k, want in w.items(): + if k in haved and haved[k] != want: + if isinstance(want, list): + for entry in want: + wname = entry.get("name") + hdict = next( + (inst for inst in haved["instances"] if inst["name"] == wname), + None, + ) + wantc = self._canonicalise(entry) + havec = self._canonicalise(hdict or {}) + + if wantc != havec: + haved["instances"] = [ + i for i in haved.get("instances", []) if i.get("name") != wname + ] + self.commands.append("delete vrf name {}".format(wname)) + self.commands.append("commit") + + for k, want in wantd.items(): + if isinstance(want, list): + self._compare_instances(want=want, have=haved.pop(k, {})) + self.compare( + parsers=self.parsers, + want={k: want}, + have={k: haved.pop(k, {})}, + ) + + def _compare_instances(self, want, have): + """Compare the instances of the VRF""" + parsers = [ + "table_id", + "vni", + "description", + "disable_vrf", + ] + + for entry in want: + h = {} + wname = entry.get("name") + h = { + k: v + for vrf in have + if vrf.get("name") == wname + for k, v in vrf.items() + if k != "address_family" + } + self.compare(parsers=parsers, want=entry, have=h) + + if "address_family" in entry: + wafi = {"name": wname, "address_family": entry.get("address_family", [])} + hdict = next((d for d in have if d.get("name") == wname), None) + + hafi = { + "name": (hdict or {"name": wname})["name"], + "address_family": hdict.get("address_family", []) if hdict else [], + } + + self._compare_addr_family(wafi, hafi) + + if "protocols" in entry: + for protocol_name in entry["protocols"]: + + w_p_dict = entry["protocols"][protocol_name] + + h_p_dict = next( + ( + v.get("protocols", {}).get(protocol_name, {}) + for v in have + if v.get("name") == wname + ), + {}, + ) + if protocol_name == "bgp": + bgp_module = Bgp_global(self._module) + bgp_module._validate_template() + bgp_module.want = w_p_dict + bgp_module.have = h_p_dict + bgp_module.generate_commands() + protocol_commands = bgp_module.commands + elif protocol_name == "ospf": + ospfv2_module = Ospfv2(self._module) + ospfv2_module._module.params["config"] = w_p_dict + ospfv2_module.state = self.state + protocol_commands = ospfv2_module.set_config(h_p_dict) + elif protocol_name == "ospfv3": + ospfv3_module = Ospfv3(self._module) + ospfv3_module._module.params["config"] = w_p_dict + ospfv3_module.state = self.state + protocol_commands = ospfv3_module.set_config(h_p_dict) + elif protocol_name == "static": + static_routes_module = Static_routes(self._module) + static_routes_module._module.params["config"] = w_p_dict + static_routes_module.state = self.state + protocol_commands = static_routes_module.set_config(h_p_dict) + else: + self._module.fail_json( + msg="The protocol {} is not supported".format(protocol_name), + ) + self.commands.extend( + [ + cmd.replace("protocols", "vrf name " + wname + " protocols", 1) + for cmd in protocol_commands + ], + ) + + def _compare_addr_family(self, want, have): + """Compare the address families of the VRF""" + afi_parsers = [ + "disable_forwarding", + "disable_nht", + ] + + wafi = self.afi_to_list(want) + hafi = self.afi_to_list(have) + + lookup = {(d["name"], d["afi"]): d for d in hafi} + pairs = [(d1, lookup.get((d1["name"], d1["afi"]), {})) for d1 in wafi] + + for wafd, hafd in pairs: + if "route_maps" in wafd: + self._compare_route_maps(wafd, hafd) + self.compare(parsers=afi_parsers, want=wafd, have=hafd) + + def afi_to_list(self, data): + """Convert address family dict to list""" + + return [ + {"name": data["name"], **{**af, "afi": "ip" if af["afi"] == "ipv4" else af["afi"]}} + for af in data["address_family"] + ] + + def _compare_route_maps(self, wafd, hafd): + want_rms = wafd.get("route_maps", []) + have_rms = hafd.get("route_maps", []) + + for want in want_rms: + match = next( + ( + h + for h in have_rms + if h["rm_name"] == want["rm_name"] and h["protocol"] == want["protocol"] + ), + {}, + ) + base = {"name": wafd["name"], "afi": wafd["afi"]} + + self.compare( + parsers="route_maps", + want={**base, "route_maps": want}, + have={**base, "route_maps": match}, + ) + + def _canonicalise(self, obj): + if isinstance(obj, dict): + return {k: self._canonicalise(v) for k, v in obj.items()} + + if isinstance(obj, list): + if not obj: + return obj + + if not isinstance(obj[0], dict): + return sorted(obj) + + canon = [self._canonicalise(i) for i in obj] + + def sort_key(d): + for k in ("name", "address", "afi", "area_id", "neighbor_id", "dest"): + if k in d: + return d[k] + return tuple(sorted(d.items())) + + return sorted(canon, key=sort_key) + + return obj diff --git a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py index 3386bd66..dab92612 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -28,11 +27,11 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_template from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( Bgp_address_familyTemplate14, ) - +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 -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_address_familyFacts(object): """The vyos bgp_address_family facts class""" diff --git a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py index dd793681..cf2e4475 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -25,15 +24,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global import ( Bgp_globalTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global_14 import ( Bgp_globalTemplate14, ) - +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 -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_globalFacts(object): """The vyos bgp_global facts class""" diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 74bbda74..92c0f97a 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -6,8 +6,8 @@ The facts class for vyos this file validates each subset of facts and selectively calls the appropriate facts gathering function """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -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, ) @@ -56,6 +59,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import ( Logging_globalFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat import ( + NatFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import ( Ntp_globalFacts, ) @@ -80,7 +86,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( Static_routesFacts, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config) FACT_RESOURCE_SUBSETS = dict( @@ -93,6 +105,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, @@ -102,8 +115,12 @@ FACT_RESOURCE_SUBSETS = dict( prefix_lists=Prefix_listsFacts, logging_global=Logging_globalFacts, ntp_global=Ntp_globalFacts, + nat=NatFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, + vrf=VrfFacts, + vpn_ipsec=Vpn_ipsecFacts, + vpn_ipsec_s2s=Vpn_ipsec_s2sFacts, ) diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py index a46f8563..e13c1939 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -77,13 +77,17 @@ class Firewall_globalFacts(object): :rtype: dictionary :returns: The generated config """ + conf = "\n".join( filter( - lambda x: ("firewall ipv6-name" and "firewall name" not in x), + lambda x: not ( + x.startswith("set firewall name") + or x.startswith("set firewall ipv6-name") + or x.startswith("set firewall ipv6 name") + ), conf, ), ) - a_lst = [ "config_trap", "validation", @@ -97,6 +101,7 @@ class Firewall_globalFacts(object): "group": self.parse_group(conf), "route_redirects": self.route_redirects(conf), "state_policy": self.parse_state_policy(conf), + "zone": self.parse_zone(conf), } firewall.update(f_sub) return firewall @@ -179,7 +184,7 @@ class Firewall_globalFacts(object): if policies: rules_lst = [] for sp in set(policies): - sp_regex = r" %s .+$" % sp + sp_regex = r"^set firewall (?:global-options )?state-policy %s .+$" % sp cfg = "\n".join(findall(sp_regex, conf, M)) obj = self.parse_policies(cfg, sp) obj["connection_type"] = sp @@ -233,7 +238,7 @@ class Firewall_globalFacts(object): if groups: rules_lst = [] for gr in set(groups): - gr_regex = r" %s .+$" % gr + gr_regex = r"^set firewall group " + type + " %s .+$" % gr cfg = "\n".join(findall(gr_regex, conf, M)) if "ipv6" in type: # fmt: off @@ -400,3 +405,120 @@ class Firewall_globalFacts(object): "twa_hazards_protection", ) return True if attrib in bool_set else False + + def parse_zone(self, conf): + """ + This function triggers the parsing of 'zone' attributes. + :param conf: configuration. + :return: generated config dictionary. + """ + cfg_dict = {} + + KEY_MAP = { + "interface": "interfaces", + "intra-zone-filtering": "intra-zone-filtering", + "from": "sources", + } + + LIST_ATTRS = { + "interfaces", + "intra_zone_filtering", + "sources", + } + + for line in conf.splitlines(): + + m = search( + r"^set firewall zone (?P<zone>\S+)\s+(?P<attr>[a-z-]+)(?:\s+(?P<value>'[^']+'|[^\n]+))?$", + line, + ) + if not m: + continue + + zone_name = m.group("zone") + raw_attr = m.group("attr").replace("-", "_") + value = m.group("value") + + if value is None: + value = True + else: + value = value.strip("'") + + # VyOS 1.5.0 GA wraps 'interface' under a new 'member' node: + # "set firewall zone <name> member interface <ifname>". Unwrap + # it here so it lands in the same 'interfaces' list as the + # pre-1.5.0 bare "interface <ifname>" form. No version check + # needed -- 1.4.x/1.5-rolling configs never emit 'member'. + if raw_attr == "member" and isinstance(value, str) and value.startswith("interface "): + raw_attr = "interface" + value = value.split(None, 1)[1].strip("'") + + zone = cfg_dict.setdefault(zone_name, {"name": zone_name}) + + attr = KEY_MAP.get(raw_attr, raw_attr) + + if attr in LIST_ATTRS: + if attr == "intra_zone_filtering": + izf = zone.setdefault(attr, {}) + izf_attr = self._parse_izf(value) + for k, v in izf_attr.items(): + if isinstance(v, dict): + izf.setdefault(k, {}).update(v) + else: + izf[k] = v + elif attr == "sources": + self._parse_sources(zone, value) + else: + zone.setdefault(attr, []).append(value) + else: + zone[attr] = value + + return list(cfg_dict.values()) + + def _parse_izf(self, value): + + tokens = value.replace("'", "").split() + + result = {} + + key = tokens[0].replace("-", "_") + + if len(tokens) == 2: + result[key] = tokens[1] + + elif len(tokens) >= 3: + subkey = tokens[1].replace("-", "_") + result[key] = {subkey: tokens[2]} + + return result + + def _parse_sources(self, zone, value): + + tokens = value.split() + + if len(tokens) < 1: + return + + src_zone = tokens[0] + + sources = zone.setdefault("sources", []) + + entry = None + for s in sources: + if s.get("zone") == src_zone: + entry = s + break + + if entry is None: + entry = {"zone": src_zone} + sources.append(entry) + + if len(tokens) == 1: + return + + if tokens[1] == "firewall" and len(tokens) >= 4: + key = tokens[2].replace("-", "_") + val = tokens[3].strip("'") + + firewall = entry.setdefault("firewall", {}) + firewall[key] = val diff --git a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py index bac31920..34235af7 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py index a6b56345..31cc1fa5 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -61,12 +61,20 @@ class Firewall_rulesFacts(object): objs = [] # check 1.4+ first new_rules = True - v6_rules = findall(r"^set firewall ipv6 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + v6_rules = findall( + r"^set firewall ipv6 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", + data, + M, + ) if not v6_rules: v6_rules = findall(r"^set firewall ipv6-name (?:\'*)(\S+)(?:\'*)", data, M) if v6_rules: new_rules = False - v4_rules = findall(r"^set firewall ipv4 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + v4_rules = findall( + r"^set firewall ipv4 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", + data, + M, + ) if not v4_rules: v4_rules = findall(r"^set firewall name (?:\'*)(\S+)(?:\'*)", data, M) if v4_rules: @@ -171,12 +179,18 @@ class Firewall_rulesFacts(object): :returns: The generated config """ conf = "\n".join(filter(lambda x: x, conf)) - a_lst = ["description", "default_action", "default_jump_target", "enable_default_log", "default_log"] + a_lst = [ + "description", + "default_action", + "default_jump_target", + "enable_default_log", + "default_log", + ] config = self.parse_attr(conf, a_lst, match) if not config: config = {} - if 'default_log' in config: - config['enable_default_log'] = config.pop('default_log') + if "default_log" in config: + config["enable_default_log"] = config.pop("default_log") config["rules"] = self.parse_rules_lst(conf) return config @@ -219,6 +233,7 @@ class Firewall_rulesFacts(object): "description", "icmp", "jump_target", + "offload_target", "queue", "queue_options", ] @@ -263,8 +278,8 @@ class Firewall_rulesFacts(object): found_lengths = findall(rule_regex, conf, M) if found_lengths: lengths = [] - for l in set(found_lengths): - obj = {"length": l.strip("'")} + for pplen in set(found_lengths): + obj = {"length": pplen.strip("'")} lengths.append(obj) return lengths @@ -389,21 +404,21 @@ class Firewall_rulesFacts(object): out = search(r"^.*" + regex + " (.+)", conf, M) if out: val = out.group(1).strip("'") - if attrib == 'type-name': - config['type_name'] = val - if attrib == 'code': - config['code'] = int(val) - if attrib == 'type': + if attrib == "type-name": + config["type_name"] = val + if attrib == "code": + config["code"] = int(val) + if attrib == "type": # <1.3 could be # (type), #/# (type/code) or 'type' (type_name) # recent this is only for strings if "/" in val: # type/code - (type_no, code) = val.split(".") - config['type'] = type_no - config['code'] = code + (type_no, code) = val.split("/") + config["type"] = int(type_no) + config["code"] = int(code) elif val.isnumeric(): - config['type'] = type_no + config["type"] = int(val) else: - config['type_name'] = val + config["type_name"] = val return config def parse_icmp(self, conf, attrib=None): @@ -414,7 +429,7 @@ class Firewall_rulesFacts(object): :return: generated config dictionary. """ cfg_dict = self.parse_icmp_attr(conf, "icmp") - if (len(cfg_dict) == 0): + if len(cfg_dict) == 0: cfg_dict = self.parse_icmp_attr(conf, "icmpv6") return cfg_dict @@ -458,7 +473,7 @@ class Firewall_rulesFacts(object): if not out: if attrib == "disable": out = search(r"^.*\d+" + " (disable$)", conf, M) - if attrib == 'log': + if attrib == "log": out = search(r"^.*\d+" + " (log$)", conf, M) if out: @@ -523,7 +538,7 @@ class Firewall_rulesFacts(object): if out: val = out.group(1).strip("'") if "/" in val: # number/unit - (number, unit) = val.split("/") - config['number'] = number - config['unit'] = unit + number, unit = val.split("/") + config["number"] = number + config["unit"] = unit return config 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/facts/hostname/hostname.py b/plugins/module_utils/network/vyos/facts/hostname/hostname.py index b4f7c529..18a66357 100644 --- a/plugins/module_utils/network/vyos/facts/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/facts/hostname/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py index cd8008c6..30d619a3 100644 --- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -80,6 +79,7 @@ class InterfacesFacts(object): facts["interfaces"].append(utils.remove_empties(cfg)) ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts def render_config(self, conf): @@ -94,7 +94,7 @@ class InterfacesFacts(object): """ vif_conf = "\n".join(filter(lambda x: ("vif" in x), conf)) eth_conf = "\n".join(filter(lambda x: ("vif" not in x), conf)) - config = self.parse_attribs(["description", "speed", "mtu", "duplex"], eth_conf) + config = self.parse_attribs(["description", "speed", "mtu", "duplex", "vrf"], eth_conf) config["vifs"] = self.parse_vifs(vif_conf) return utils.remove_empties(config) diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py index 7d4d1a08..9e79e24d 100644 --- a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -20,7 +19,6 @@ import re from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.l3_interfaces.l3_interfaces import ( @@ -139,7 +137,7 @@ class L3_interfacesFacts(object): else: config["ipv6"].append({"address": item}) - for key, value in iteritems(config): + for key, value in config.items(): if value == []: config[key] = None diff --git a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py index 8e1c8624..94c73416 100644 --- a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/legacy/base.py b/plugins/module_utils/network/vyos/facts/legacy/base.py index 30978e0e..59666e19 100644 --- a/plugins/module_utils/network/vyos/facts/legacy/base.py +++ b/plugins/module_utils/network/vyos/facts/legacy/base.py @@ -11,7 +11,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type import platform import re diff --git a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py index 7a6e9b8e..40ff2ad3 100644 --- a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py index e029b47a..72bdf154 100644 --- a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py index 8b60bef9..243043bc 100644 --- a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -15,7 +14,6 @@ for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.logging_global.logging_global import ( @@ -24,6 +22,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.log from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import ( Logging_globalTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import ( + Logging_globalTemplate15, +) +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 Logging_globalFacts(object): @@ -38,7 +43,7 @@ class Logging_globalFacts(object): def process_facts(self, objFinal): if objFinal: - for ke, vl in iteritems(objFinal): + for ke, vl in objFinal.items(): if ke == "files": _files = [] for k, v in vl.items(): @@ -81,8 +86,17 @@ class Logging_globalFacts(object): if not data: data = self.get_logging_data(connection) - # parse native config using the Logging_global template - logging_global_parser = Logging_globalTemplate(lines=data.splitlines(), module=self._module) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"): + logging_global_parser = Logging_globalTemplate15( + lines=data.splitlines(), + module=self._module, + ) + else: + logging_global_parser = Logging_globalTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = logging_global_parser.parse() ansible_facts["ansible_network_resources"].pop("logging_global", None) objs = self.process_facts(objs) diff --git a/plugins/module_utils/network/vyos/facts/nat/__init__.py b/plugins/module_utils/network/vyos/facts/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py new file mode 100644 index 00000000..73d6238f --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/nat.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# 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 + +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.nat.nat import ( + NatArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import ( + NatTemplate, +) + + +class NatFacts(object): + """The vyos nat facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = NatArgs.argument_spec + + def get_config(self, connection): + return connection.get("show configuration commands | match 'set nat'") + + def populate_facts(self, connection, ansible_facts, data=None): + facts = {} + config_lines = [] + + if not data: + data = self.get_config(connection) + + for resource in data.splitlines(): + config_lines.append(re.sub(r"'([^']*)'", r"\1", resource)) + + nat_parser = NatTemplate(lines=config_lines, module=self._module) + objs = nat_parser.parse() + objs = self._normalise(objs) + + ansible_facts["ansible_network_resources"].pop("nat", None) + + params = utils.remove_empties( + nat_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + if params.get("config"): + facts["nat"] = params["config"] + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def _deep_merge(self, base, override): + for k, v in override.items(): + if k in base and isinstance(base[k], dict) and isinstance(v, dict): + self._deep_merge(base[k], v) + elif k in base and isinstance(base[k], list) and isinstance(v, list): + for entry in v: + if entry not in base[k]: + base[k].append(entry) + else: + base[k] = v + return base + + def _merge_rule_list(self, rules): + merged = {} + for item in rules: + rid = item["id"] + if rid not in merged: + merged[rid] = {"id": rid} + for k, v in item.items(): + if k == "id": + continue + if isinstance(v, list): + existing = merged[rid].setdefault(k, []) + for entry in v: + if entry not in existing: + existing.append(entry) + elif isinstance(v, dict): + merged[rid].setdefault(k, {}) + self._deep_merge(merged[rid][k], v) + else: + merged[rid][k] = v + return list(merged.values()) + + def _merge_pool_list(self, pools): + merged = {} + for item in pools: + name = item["name"] + if name not in merged: + merged[name] = {"name": name} + for k, v in item.items(): + if k == "name": + continue + if k == "range" and isinstance(v, list): + existing = merged[name].setdefault(k, []) + existing.extend(v) + if v and isinstance(v[0], dict): + merged[name][k] = self._merge_range_list(existing) + else: + merged[name][k] = list(dict.fromkeys(existing)) + elif isinstance(v, list): + merged[name].setdefault(k, []) + for val in v: + if val not in merged[name][k]: + merged[name][k].append(val) + elif isinstance(v, dict): + merged[name].setdefault(k, {}) + self._deep_merge(merged[name][k], v) + else: + merged[name][k] = v + return list(merged.values()) + + def _merge_range_list(self, ranges): + """Merge external pool range entries by value, preserving seq.""" + merged = {} + for entry in ranges: + if isinstance(entry, dict): + key = entry.get("value") or entry.get("address", "") + if not key: + continue + if key not in merged: + merged[key] = {"value": key} + if entry.get("seq"): + merged[key]["seq"] = entry["seq"] + else: + if entry not in merged: + merged[entry] = {"value": entry} + return list(merged.values()) + + def _normalise(self, objs): + for nat_type in ["nat", "nat64", "nat66"]: + nat = objs.get(nat_type) + if not nat: + continue + + for section in ["destination", "source", "static", "cgnat"]: + if section not in nat: + continue + rules = nat[section].get("rule") + if isinstance(rules, list): + nat[section]["rule"] = self._merge_rule_list(rules) + nat[section]["rule"].sort(key=lambda x: x.get("id", 0)) + + if "cgnat" in nat and "pool" in nat["cgnat"]: + pool = nat["cgnat"]["pool"] + for ptype in ["external", "internal"]: + if ptype in pool and isinstance(pool[ptype], list): + pool[ptype] = self._merge_pool_list(pool[ptype]) + + if nat_type == "nat64": + for rule in nat.get("source", {}).get("rule", []): + pools = rule.get("translation", {}).get("pool") + if pools and isinstance(pools, list): + rule["translation"]["pool"] = self._merge_rule_list(pools) + rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0)) + + self._cast_ports(objs) + return objs + + def _cast_ports(self, obj): + """Recursively cast known integer port/seq fields to str.""" + if isinstance(obj, dict): + for k, v in obj.items(): + if k in ("port", "seq") and isinstance(v, int): + obj[k] = str(v) + else: + self._cast_ports(v) + elif isinstance(obj, list): + for item in obj: + self._cast_ports(item) diff --git a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py index 880bc79f..a6d6892f 100644 --- a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py index 852e1da7..1a9a687d 100644 --- a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -23,17 +22,16 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.osp Ospf_interfacesArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces import ( - Ospf_interfacesTemplate + Ospf_interfacesTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( - Ospf_interfacesTemplate14 + Ospf_interfacesTemplate14, +) +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 -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Ospf_interfacesFacts(object): """The vyos ospf_interfaces facts class""" @@ -54,7 +52,9 @@ class Ospf_interfacesFacts(object): for config_line in data.splitlines(): ospf_int = re.search(r"set protocols (?:ospf|ospfv3) interface (\S+).*", config_line) if ospf_int: - config_dict[ospf_int.group(1)] = config_dict.get(ospf_int.group(1), "") + config_line + "\n" + config_dict[ospf_int.group(1)] = ( + config_dict.get(ospf_int.group(1), "") + config_line + "\n" + ) return list(config_dict.values()) def get_config_set_1_2(self, data): @@ -63,12 +63,15 @@ class Ospf_interfacesFacts(object): config_set = [] int_string = "" for config_line in data.splitlines(): - ospf_int = re.search(r"set interfaces \S+ (\S+) .*", config_line) + ospf_int_raw = re.findall(r"^set interfaces \S+ (\S+)", config_line, re.M) + ospf_int_vif = re.findall(r"^set interfaces \S+ (\S+) vif (\d+)", config_line, re.M) + + ospf_int = ospf_int_raw + ospf_int_vif if ospf_int: - if ospf_int.group(1) not in interface_list: + if ospf_int not in interface_list: if int_string: config_set.append(int_string) - interface_list.append(ospf_int.group(1)) + interface_list.append(ospf_int) int_string = "" int_string = int_string + config_line + "\n" if int_string: @@ -115,7 +118,6 @@ class Ospf_interfacesFacts(object): if key in objs and objs[key]: objs[key] = list(objs[key].values()) ospf_interfaces_facts.append(objs) - ansible_facts["ansible_network_resources"].pop("ospf_interfaces", None) facts = {"ospf_interfaces": []} params = utils.remove_empties( @@ -123,11 +125,10 @@ class Ospf_interfacesFacts(object): self.argument_spec, {"config": ospf_interfaces_facts}, redact=True, - ) + ), ) if params.get("config"): for cfg in params["config"]: facts["ospf_interfaces"].append(utils.remove_empties(cfg)) ansible_facts["ansible_network_resources"].update(facts) - return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py index bdc7c9f8..056f949d 100644 --- a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -22,6 +22,10 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common i from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv2.ospfv2 import ( Ospfv2Args, ) +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 Ospfv2Facts(object): @@ -99,9 +103,9 @@ class Ospfv2Facts(object): config["areas"] = self.parse_attrib_list(conf, "area", "area_id") config["parameters"] = self.parse_attrib(conf, "parameters", "parameters") config["neighbor"] = self.parse_attrib_list(conf, "neighbor", "neighbor_id") - config["passive_interface"] = self.parse_leaf_list(conf, "passive-interface") + config["passive_interface"] = self.parse_passive(conf, "passive-interface") config["redistribute"] = self.parse_attrib_list(conf, "redistribute", "route_type") - config["passive_interface_exclude"] = self.parse_leaf_list( + config["passive_interface_exclude"] = self.parse_passive( conf, "passive-interface-exclude", ) @@ -159,6 +163,29 @@ class Ospfv2Facts(object): lst.sort() return lst + def parse_passive(self, conf, attrib): + """ + This function forms the regex to fetch the listed attributes + from the configuration data + :param conf: configuration data + :param attrib: attribute name + :return: generated rule list configuration + """ + lst = [] + items = [] + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if attrib == "passive-interface-exclude": + items = findall("^interface (?:'*)(\\S+)(?:'*) passive disable$", conf, M) + else: + items = findall("^interface (?:'*)(\\S+)(?:'*) passive$", conf, M) + + items += findall(r"^" + attrib + " (?:'*)(\\S+)(?:'*)", conf, M) + if items: + for i in set(items): + lst.append(i.strip("'")) + lst.sort() + return lst + def parse_distance(self, conf, attrib=None): """ This function triggers the parsing of 'distance' attributes @@ -389,13 +416,13 @@ class Ospfv2Facts(object): :param match: parent node/attribute name. :return: generated config dictionary. """ - config = {} for attrib in attr_list: regex = self.map_regex(attrib) if match: regex = match.replace("_", "-") + " " + regex + if conf: if self.is_bool(attrib): out = conf.find(attrib.replace("_", "-")) @@ -403,13 +430,13 @@ class Ospfv2Facts(object): if match: if attrib == "set" and conf.find(match) >= 1: config[attrib] = True - en = conf.find(match + " 'enable'") + en = conf.find(match + " enable") != -1 if out >= 1: if dis >= 1: config[attrib] = False else: config[attrib] = True - elif match and en >= 1: + elif match and en: config[attrib] = True else: out = search(r"^.*" + regex + " (.+)", conf, M) diff --git a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py index 547ff793..5fd9e627 100644 --- a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py @@ -9,8 +9,8 @@ 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. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -22,6 +22,10 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common i from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv3.ospfv3 import ( Ospfv3Args, ) +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 Ospfv3Facts(object): @@ -100,6 +104,9 @@ class Ospfv3Facts(object): for item in set(items): i_regex = r" %s .+$" % item cfg = "\n".join(findall(i_regex, conf, M)) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + cfg14 = findall(r"(interface .+) area '%s'$" % item, conf, M) + cfg += "\n " + item + " " + " ".join(cfg14) if attrib == "area": obj = self.parse_area(cfg, item) else: @@ -121,6 +128,8 @@ class Ospfv3Facts(object): rule = self.parse_attrib(conf, "area_id", match=area_id) r_sub = {"range": self.parse_attrib_list(conf, "range", "address")} rule.update(r_sub) + r_int = {"interface": self.parse_attrib_list(conf, "interface", "name")} + rule.update(r_int) return rule def parse_attrib(self, conf, param, match=None): @@ -133,6 +142,7 @@ class Ospfv3Facts(object): "area_id": ["export_list", "import_list"], "redistribute": ["route_map"], "range": ["advertise", "not_advertise"], + "interface": ["name"], "parameters": ["router_id"], } cfg_dict = self.parse_attr(conf, param_lst[param], match) diff --git a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py index 17f63fb2..4bdcbf8c 100644 --- a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py index 2ad54e63..d4084fd7 100644 --- a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -25,6 +24,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.rou from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps import ( Route_mapsTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +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 Route_mapsFacts(object): @@ -59,11 +65,18 @@ class Route_mapsFacts(object): """ facts = {} objs = [] + + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + route_maps_class = Route_mapsTemplate14 + else: + route_maps_class = Route_mapsTemplate + if not data: data = self.get_config(connection) # parse native config using the Route_maps template - route_maps_parser = Route_mapsTemplate(lines=data.splitlines()) + route_maps_parser = route_maps_class(lines=data.splitlines()) + if route_maps_parser.parse().get("route_maps"): objs = list(route_maps_parser.parse().get("route_maps").values()) for item in objs: diff --git a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py index e70a15f9..d3ff02e5 100644 --- a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ based on the configuration. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.snmp_server.snmp_server import ( @@ -63,7 +61,7 @@ class Snmp_serverFacts(object): if objs: if "communities" in objs: for k in objs["communities"].values(): - for param, val in iteritems(k): + for param, val in k.items(): if param in ["clients", "networks"]: if None in val: val.remove(None) diff --git a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py index 99b3917b..1bce772c 100644 --- a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy from re import M, findall, search @@ -164,9 +163,9 @@ class Static_routesFacts(object): elif dis >= 1: nh_info["enabled"] = False for element in nh_list: - if element["forward_router_address"] == nh_info["forward_router_address"]: - if "interface" in nh_info.keys(): - element["interface"] = nh_info["interface"] + if element.get("forward_router_address") == nh_info.get( + "forward_router_address", + ): if "admin_distance" in nh_info.keys(): element["admin_distance"] = nh_info["admin_distance"] if "enabled" in nh_info.keys(): @@ -174,4 +173,5 @@ class Static_routesFacts(object): nh_info = None if nh_info is not None: nh_list.append(nh_info) + nh_info = {} return nh_list diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..695b9b8d --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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 vpn_ipsec 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. + +Follows the established per-key conversion convention used by +vyos_logging_global/vyos_ha (explicit process_facts() naming each +name-keyed dict that needs converting to a list), matching the config.py +convention for this module, rather than a generic argspec-driven walker. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( + Vpn_ipsecTemplate, +) + + +class Vpn_ipsecFacts(object): + """The vyos vpn_ipsec facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vpn_ipsecArgs.argument_spec + + def get_vpn_ipsec_data(self, connection): + return connection.get('show configuration commands | match "vpn ipsec"') + + def process_facts(self, objFinal): + """Convert the name-keyed dicts produced by the parser into the + lists the argspec expects. Each key handled explicitly, matching + the vyos_logging_global/vyos_ha convention. + """ + if not objFinal: + return objFinal + + for key in ("ike_group", "esp_group"): + if key in objFinal: + items = list(objFinal[key].values()) + for item in items: + if "proposal" in item: + item["proposal"] = sorted( + item["proposal"].values(), + key=lambda p: int(p["proposal_id"]), + ) + objFinal[key] = sorted(items, key=lambda item: item["name"]) + + if "profile" in objFinal: + objFinal["profile"] = sorted( + objFinal["profile"].values(), + key=lambda item: item["name"], + ) + + if "authentication" in objFinal: + auth = objFinal["authentication"] + for key in ("psk", "ppk"): + if key in auth: + auth[key] = sorted( + auth[key].values(), + key=lambda item: item["name"], + ) + + return objFinal + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vpn_ipsec network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_vpn_ipsec_data(connection) + + vpn_ipsec_parser = Vpn_ipsecTemplate(lines=data.splitlines(), module=self._module) + objs = vpn_ipsec_parser.parse() + + ansible_facts["ansible_network_resources"].pop("vpn_ipsec", None) + objs = self.process_facts(objs) + + params = utils.remove_empties( + vpn_ipsec_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vpn_ipsec"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..6c9b5d8e --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,115 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 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 vpn_ipsec_s2s 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. + +Follows the established per-key conversion convention used by +vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts() +naming each name-keyed dict that needs converting to a list), matching +the config.py convention for this module, rather than a generic +argspec-driven walker. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2sFacts(object): + """The vyos vpn_ipsec_s2s facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec + + def get_vpn_ipsec_s2s_data(self, connection): + return connection.get( + 'show configuration commands | match "vpn ipsec site-to-site"', + ) + + def process_facts(self, objFinal): + """Convert the name-keyed dicts produced by the parser into the + lists the argspec expects. + + NOTE: every PARSERS result template in rm_templates.py nests its + output under "site_to_site" -> "peer" (mirroring the CLI's own + tree: `vpn ipsec site-to-site peer <name> ...`), but the + argspec's `config` has `peer` directly at the top level -- there + is no `site_to_site` wrapper in the argspec, since that's the + one node wrap_docstring.py unwrapped when building the + docstring (its own children became config's children directly). + So this needs to strip that outer key, not just convert the + name-keyed dicts to lists. + """ + if not objFinal: + return objFinal + + site_to_site = objFinal.get("site_to_site", {}) + peers = site_to_site.get("peer", {}) + + items = list(peers.values()) + for item in items: + if "tunnel" in item: + item["tunnel"] = sorted( + item["tunnel"].values(), + key=lambda t: int(t["tunnel_id"]), + ) + + return {"peer": sorted(items, key=lambda item: item["name"])} + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vpn_ipsec_s2s network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_vpn_ipsec_s2s_data(connection) + + vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = vpn_ipsec_s2s_parser.parse() + + ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None) + objs = self.process_facts(objs) + + params = utils.remove_empties( + vpn_ipsec_s2s_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vpn_ipsec_s2s"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/vrf/__init__.py b/plugins/module_utils/network/vyos/facts/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vrf/vrf.py b/plugins/module_utils/network/vyos/facts/vrf/vrf.py new file mode 100644 index 00000000..74f46b45 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vrf/vrf.py @@ -0,0 +1,226 @@ +# -*- 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 vrf 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.vrf.vrf import VrfArgs +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_global.bgp_global import ( + Bgp_globalFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv2.ospfv2 import ( + Ospfv2Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv3.ospfv3 import ( + Ospfv3Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( + Static_routesFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import ( + VrfTemplate, +) + + +class VrfFacts(object): + """The vyos vrf facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = VrfArgs.argument_spec + + def get_config(self, connection): + return connection.get("show configuration commands | match 'set vrf'") + + def get_config_set(self, data, connection): + """To classify the configurations beased on vrf""" + config_dict = {} + for config_line in data.splitlines(): + vrf_inst = re.search(r"set vrf name (\S+).*", config_line) + vrf_bta = re.search(r"set vrf bind-to-all", config_line) + if vrf_bta: + config_dict["bind_to_all"] = config_dict.get("bind_to_all", "") + config_line + "\n" + if vrf_inst: + config_dict[vrf_inst.group(1)] = ( + config_dict.get(vrf_inst.group(1), "") + config_line + "\n" + ) + return list(config_dict.values()) + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vrf 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) + + vrf_facts = {} + instances = [] + vrf_parser = VrfTemplate(lines=[], module=self._module) + resources = self.get_config_set(data, connection) + + for resource in resources: + vrf_parser = VrfTemplate( + lines=resource.split("\n"), + module=self._module, + ) + objs = vrf_parser.parse() + + if objs and "protocols" in resource: + + protocol_lines = [] + for line in resource.strip().split("\n"): + if "protocols" in line: + idx = line.index("protocols") + protocol_lines.append("set " + line[idx:]) + objs["protocols"] = self._parse_protocols("\n".join(protocol_lines)) + + if objs: + if "bind_to_all" in objs: + vrf_facts.update(objs) + if "name" in objs: + instances.append(self._normalise_instance(objs)) + + if instances: + vrf_facts.update({"instances": instances}) + + ansible_facts["ansible_network_resources"].pop("vrf_facts", None) + facts = {"vrf": []} + + params = utils.remove_empties( + vrf_parser.validate_config( + self.argument_spec, + {"config": vrf_facts}, + redact=True, + ), + ) + + if not resources: + params["config"].pop("bind_to_all", None) + + if params.get("config"): + facts["vrf"] = params["config"] + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def _normalise_instance(self, instance): + n_inst = instance.copy() + af_map = {} + + for af in instance.get("address_family", []): + afi = af.get("afi") + if not afi: + continue + + if afi not in af_map: + af_map[afi] = {"afi": afi} + + for k, v in af.items(): + if k == "afi": + continue + elif k == "route_maps": + if "route_maps" not in af_map[afi]: + af_map[afi]["route_maps"] = [] + af_map[afi]["route_maps"].extend(v) + else: + af_map[afi][k] = v + + for afi_data in af_map.values(): + if "route_maps" in afi_data: + seen = [] + deduped = [] + for item in afi_data["route_maps"]: + if item not in seen: + seen.append(item) + deduped.append(item) + afi_data["route_maps"] = deduped + + n_inst["address_family"] = list(af_map.values()) + return n_inst + + def _parse_protocols(self, protocols): + """Parse protocols and return a dictionary""" + + protocol_chunks = {} + parsed_protocols = {} + + for line in protocols.split("\n"): + parts = line.split() + if len(parts) > 2 and parts[0] == "set" and parts[1] == "protocols": + protocol = parts[2] + protocol_chunks.setdefault(protocol, []).append(line) + + protocol_strings = {proto: "\n".join(lines) for proto, lines in protocol_chunks.items()} + + for protocol_name, protocol_string in protocol_strings.items(): + protocol_dict = {} + + if protocol_name == "bgp": + bgp_module = Bgp_globalFacts(self._module) + protocol_dict = bgp_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "ospf": + ospf_module = Ospfv2Facts(self._module) + protocol_dict = ospf_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "ospfv3": + ospfv3_module = Ospfv3Facts(self._module) + protocol_dict = ospfv3_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "static": + static_routes_module = Static_routesFacts(self._module) + protocol_dict = static_routes_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + else: + self._module.fail_json(msg="The protocol is not supported" + protocol_name) + + return parsed_protocols diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py index f8f86cd2..fe7889aa 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -293,8 +292,8 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+aggregate-address \s+(?P<address>\S+) - \s*(?P<as_set>as-set)* - \s*(?P<summary_only>summary-only)* + \s*(?P<as_set>as-set)? + \s*(?P<summary_only>summary-only)? $""", re.VERBOSE, ), @@ -397,8 +396,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+path-limit - \s+(?P<limit>\S+) - *$""", + \s+(?P<limit>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -432,8 +430,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -499,8 +496,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+metric - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -534,8 +530,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -568,8 +563,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+table - \s+(?P<tab>\S+) - *$""", + \s+(?P<tab>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -659,8 +653,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+allowas-in \s+number - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -877,8 +870,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+capability \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -915,8 +907,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -992,8 +983,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1033,8 +1023,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1073,8 +1062,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1176,8 +1164,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1246,8 +1233,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1389,8 +1375,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1424,8 +1409,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py index fd4c9de9..9936cf6d 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -309,8 +308,8 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+aggregate-address \s+(?P<address>\S+) - \s*(?P<as_set>as-set)* - \s*(?P<summary_only>summary-only)* + \s*(?P<as_set>as-set)? + \s*(?P<summary_only>summary-only)? $""", re.VERBOSE, ), @@ -410,8 +409,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+path-limit - \s+(?P<limit>\S+) - *$""", + \s+(?P<limit>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -444,8 +442,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -508,8 +505,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+(?P<proto>\S+) - \s+metric\s+(?P<val>\S+) - *$""", + \s+metric\s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -542,8 +538,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -575,8 +570,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+table - \s+(?P<tab>\S+) - *$""", + \s+(?P<tab>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -663,8 +657,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+allowas-in \s+number - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -875,8 +868,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+capability \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -912,8 +904,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -987,8 +978,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1027,8 +1017,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1066,8 +1055,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1166,8 +1154,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1234,8 +1221,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1373,8 +1359,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1407,8 +1392,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py index 621f65ea..730fa5ee 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ the given network resource. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( NetworkTemplate, ) @@ -26,7 +24,7 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r def _tmplt_bgp_params_confederation(config_data): command = [] for list_el in config_data["bgp_params"]["confederation"]: - for k, v in iteritems(list_el): + for k, v in list_el.items(): command.append( "protocols bgp {as_number} parameters confederation ".format(**config_data) + k @@ -65,7 +63,7 @@ def _tmplt_bgp_params_default(config_data): def _tmplt_bgp_neighbor_timers(config_data): command = [] - for k, v in iteritems(config_data["neighbor"]["timers"]): + for k, v in config_data["neighbor"]["timers"].items(): command.append( "protocols bgp {as_number} neighbor ".format(**config_data) + config_data["neighbor"]["address"] @@ -80,7 +78,7 @@ def _tmplt_bgp_neighbor_timers(config_data): def _tmplt_bgp_timers(config_data): command = [] - for k, v in iteritems(config_data["timers"]): + for k, v in config_data["timers"].items(): command.append( "protocols bgp {as_number} ".format(**config_data) + "timers " + k + " " + str(v), ) @@ -238,8 +236,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+advertisement-interval - \s+(?P<interval>\S+) - *$""", + \s+(?P<interval>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -438,8 +435,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -468,8 +464,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -495,8 +490,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+description - \s+(?P<desc>\S+) - *$""", + \s+(?P<desc>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -574,8 +568,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+disable-send-community - \s+(?P<comm>\S+) - *$""", + \s+(?P<comm>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -634,8 +627,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ebgp-multihop - \s+(?P<hop>\S+) - *$""", + \s+(?P<hop>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -662,8 +654,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -722,8 +713,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -827,8 +817,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+password - \s+(?P<pwd>\S+) - *$""", + \s+(?P<pwd>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -854,8 +843,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -881,8 +869,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+port - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -909,8 +896,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -941,8 +927,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+remote-as - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -995,8 +980,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1158,8 +1142,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1185,8 +1168,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+update-source - \s+(?P<src>\S+) - *$""", + \s+(?P<src>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1212,8 +1194,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1239,8 +1220,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ttl-security - \s+(?P<ttl>\S+) - *$""", + \s+(?P<ttl>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1267,8 +1247,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+timers \s+(?P<type>connect|holdtime|keepalive) - \s+(?P<sec>\S+) - *$""", + \s+(?P<sec>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1296,8 +1275,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+timers \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1419,8 +1397,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+cluster-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1443,8 +1420,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+confederation \s+(?P<type>identifier|peers) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1472,8 +1448,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1498,8 +1473,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1524,8 +1498,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1550,8 +1523,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1575,9 +1547,8 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+default - \s*(?P<no_ipv4_unicast>no-ipv4-unicast)* - \s*(?P<local_pref>local-pref\s\S+) - *$""", + \s*(?P<no_ipv4_unicast>no-ipv4-unicast)? + \s*(?P<local_pref>local-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1649,8 +1620,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+distance\sprefix \s+(?P<prefix>\S+) \s+distance - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1679,8 +1649,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1730,8 +1699,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1819,8 +1787,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+router-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters router-id {{ bgp_params.router_id }}", diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py index b8beb923..b86e233c 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ the given network resource. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( NetworkTemplate, ) @@ -26,12 +24,9 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r def _tmplt_bgp_params_confederation(config_data): command = [] for list_el in config_data["bgp_params"]["confederation"]: - for k, v in iteritems(list_el): + for k, v in list_el.items(): command.append( - "protocols bgp parameters confederation ".format(**config_data) - + k - + " " - + str(v), + "protocols bgp parameters confederation ".format(**config_data) + k + " " + str(v), ) return command @@ -65,7 +60,7 @@ def _tmplt_bgp_params_default(config_data): def _tmplt_bgp_neighbor_timers(config_data): command = [] - for k, v in iteritems(config_data["neighbor"]["timers"]): + for k, v in config_data["neighbor"]["timers"].items(): command.append( "protocols bgp neighbor ".format(**config_data) + config_data["neighbor"]["address"] @@ -80,7 +75,7 @@ def _tmplt_bgp_neighbor_timers(config_data): def _tmplt_bgp_timers(config_data): command = [] - for k, v in iteritems(config_data["timers"]): + for k, v in config_data["timers"].items(): command.append( "protocols bgp ".format(**config_data) + "timers " + k + " " + str(v), ) @@ -238,8 +233,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+advertisement-interval - \s+(?P<interval>\S+) - *$""", + \s+(?P<interval>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -431,8 +425,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -460,8 +453,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -486,8 +478,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+description - \s+(?P<desc>\S+) - *$""", + \s+(?P<desc>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -562,8 +553,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+disable-send-community - \s+(?P<comm>\S+) - *$""", + \s+(?P<comm>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -620,8 +610,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ebgp-multihop - \s+(?P<hop>\S+) - *$""", + \s+(?P<hop>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -647,8 +636,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -705,8 +693,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -806,8 +793,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+password - \s+(?P<pwd>\S+) - *$""", + \s+(?P<pwd>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -832,8 +818,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -858,8 +843,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+port - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -885,8 +869,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -916,8 +899,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+remote-as - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -968,8 +950,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1125,8 +1106,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1151,8 +1131,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+update-source - \s+(?P<src>\S+) - *$""", + \s+(?P<src>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1177,8 +1156,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1203,8 +1181,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ttl-security - \s+(?P<ttl>\S+) - *$""", + \s+(?P<ttl>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1230,8 +1207,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+timers \s+(?P<type>connect|holdtime|keepalive) - \s+(?P<sec>\S+) - *$""", + \s+(?P<sec>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1258,8 +1234,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+timers \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1376,8 +1351,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+cluster-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1399,8 +1373,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+confederation \s+(?P<type>identifier|peers) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1427,8 +1400,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1452,8 +1424,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1477,8 +1448,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1502,8 +1472,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1526,9 +1495,8 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+default - \s*(?P<no_ipv4_unicast>no-ipv4-unicast)* - \s*(?P<local_pref>local-pref\s\S+) - *$""", + \s*(?P<no_ipv4_unicast>no-ipv4-unicast)? + \s*(?P<local_pref>local-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1597,8 +1565,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+distance\sprefix \s+(?P<prefix>\S+) \s+distance - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1626,8 +1593,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1675,8 +1641,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1760,8 +1725,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+router-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters router-id {{ bgp_params.router_id }}", 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/rm_templates/hostname.py b/plugins/module_utils/network/vyos/rm_templates/hostname.py index 29ab00f6..b7e56310 100644 --- a/plugins/module_utils/network/vyos/rm_templates/hostname.py +++ b/plugins/module_utils/network/vyos/rm_templates/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global.py b/plugins/module_utils/network/vyos/rm_templates/logging_global.py index 516e270b..07ad1f68 100644 --- a/plugins/module_utils/network/vyos/rm_templates/logging_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py new file mode 100644 index 00000000..3216747d --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py @@ -0,0 +1,241 @@ +# -*- 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 + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def tmplt_params(config_data): + def templt_common(val, tmplt): + if val.get("facility"): + tmplt += " facility {facility}".format(facility=val["facility"]) + if val.get("severity"): + tmplt += " level {level}".format(level=val["severity"]) + return tmplt + + tmplt = "" + if config_data.get("global_params"): + val = config_data.get("global_params") + tmplt += "system syslog local" + tmplt = templt_common(val.get("facilities", {}), tmplt) + elif config_data.get("console"): + val = config_data.get("console") + tmplt += "system syslog console" + tmplt = templt_common(val.get("facilities", {}), tmplt) + elif config_data.get("hosts"): + val = config_data.get("hosts") + if val.get("hostname") and not val.get("port") and not val.get("protocol"): + tmplt += "system syslog remote {hostname}".format(hostname=val["hostname"]) + if val.get("facilities"): + tmplt = templt_common(val.get("facilities"), tmplt) + return tmplt + + +class Logging_globalTemplate15(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Logging_globalTemplate15, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "syslog.state", + "getval": re.compile( + r""" + ^set\ssystem + (\s(?P<syslog>syslog)) + $""", re.VERBOSE, + ), + "setval": "system syslog", + "result": { + "syslog": { + "state": "{{ 'enabled' if syslog is defined else 'disabled' }}", + }, + }, + }, + { + "name": "console.state", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<console>console)) + $""", re.VERBOSE, + ), + "setval": "system syslog console", + "result": { + "console": { + "state": "{{ 'enabled' if console is defined else 'disabled' }}", + }, + }, + }, + { + "name": "console.facilities", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sconsole\sfacility + (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))? + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog console facility {{ console.facilities.facility }}", + "result": { + "console": { + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + { + "name": "global_params.state", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<local>local)) + $""", re.VERBOSE, + ), + "setval": "system syslog local", + "result": { + "global_params": { + "state": "{{ 'enabled' if local is defined else 'disabled' }}", + }, + }, + }, + { + "name": "global_params.marker_interval", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\smarker\sinterval + (\s(?P<marker_interval>'(\d+)'))? + $""", re.VERBOSE, + ), + "setval": "system syslog marker interval {{ global_params.marker_interval }}", + "remval": "system syslog marker", + "result": { + "global_params": { + "marker_interval": "{{ marker_interval }}", + }, + }, + }, + { + "name": "global_params.preserve_fqdn", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<preserve_fqdn>preserve-fqdn)) + $""", re.VERBOSE, + ), + "setval": "system syslog preserve-fqdn", + "result": { + "global_params": { + "preserve_fqdn": "{{ True if preserve_fqdn is defined }}", + }, + }, + }, + { + "name": "global_params.facilities", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\slocal\sfacility + (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))? + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog local facility {{ global_params.facilities.facility }}", + "result": { + "global_params": { + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + { + "name": "hosts.port", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sport\s(?P<port>'(\d+)')) + $""", re.VERBOSE, + ), + "setval": "system syslog remote {{ hosts.hostname }} port {{ hosts.port }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "port": "{{ port }}", + }, + }, + }, + }, + { + "name": "hosts.protocol", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sprotocol\s(?P<protocol>'(udp|tcp)')) + $""", re.VERBOSE, + ), + "setval": "system syslog remote {{ hosts.hostname }} protocol {{ hosts.protocol }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "protocol": "{{ protocol }}", + }, + }, + }, + }, + { + "name": "hosts", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sfacility\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7])) + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog remote {{ hosts.hostname }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py new file mode 100644 index 00000000..94af8696 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/nat.py @@ -0,0 +1,1186 @@ +# -*- coding: utf-8 -*- +# 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 + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_nat_rule_addr_sub(config_data): + """Generate address/fqdn/prefix/port/group commands for destination or source sub-dict.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + atype = config_data["atype"] + sub = config_data["sub"] + + base = f"{nat} {type_} rule {rid} {atype}" + commands = [] + + for field in ("address", "fqdn", "prefix", "port"): + if sub.get(field) is not None: + commands.append(f"{base} {field} {sub[field]}") + + for gtype in ("address_group", "domain_group", "mac_group", "network_group", "port_group"): + if sub.get(gtype) is not None: + commands.append(f"{base} group {gtype.replace('_', '-')} {sub[gtype]}") + + return commands + + +def _tmplt_nat_rule_translation(config_data): + """Generate translation commands.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + trans = config_data["translation"] + + base = f"{nat} {type_} rule {rid} translation" + commands = [] + + if trans.get("address") is not None: + commands.append(f"{base} address {trans['address']}") + + if trans.get("port") is not None: + commands.append(f"{base} port {trans['port']}") + + if trans.get("redirect_port") is not None: + commands.append(f"{base} redirect port {trans['redirect_port']}") + + if trans.get("address_mapping") is not None: + commands.append(f"{base} options address-mapping {trans['address_mapping']}") + + if trans.get("port_mapping") is not None: + commands.append(f"{base} options port-mapping {trans['port_mapping']}") + + return commands + + +def _tmplt_nat64_translation_pool(config_data): + """Generate all nat64 translation pool commands from a single call.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + pool_id = config_data["pool_id"] + pool = config_data["pool"] + + base = f"{nat} {type_} rule {rid} translation pool {pool_id}" + commands = [] + + if pool.get("address") is not None: + commands.append(f"{base} address {pool['address']}") + if pool.get("description") is not None: + commands.append(f"{base} description '{pool['description']}'") + if pool.get("port") is not None: + commands.append(f"{base} port {pool['port']}") + if pool.get("protocol") is not None: + commands.append(f"{base} protocol {pool['protocol']}") + if pool.get("disable"): + commands.append(f"{base} disable") + + return commands + + +class NatTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(NatTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module) + + # fmt: off + PARSERS = [ + + # ------------------------- + # CGNAT + # ------------------------- + { + "name": "cgnat_log_allocation", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+log-allocation + $""", + re.VERBOSE, + ), + "setval": "nat cgnat log-allocation", + "result": { + "nat": { + "cgnat": { + "log_allocation": True, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+range + \s+(?P<range>\S+)(?:\s+seq\s+(?P<seq>\d+))? + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined and seq %} seq {{ seq }}{% endif %}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "range": [ + { + "value": "{{ range }}", + "seq": "{{ seq }}", + }, + ], + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_port_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+external-port-range + \s+(?P<range>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} external-port-range {{ range }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "external_port_range": "{{ range }}", + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_per_user", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+per-user-limit + \s+port + \s+(?P<limit>\d+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} per-user-limit port {{ limit }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "per_user_limit": {"port": "{{ limit }}"}, + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_internal_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+internal + \s+(?P<name>\S+) + \s+range + \s+(?P<range>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool internal {{ name }} range {{ range }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "internal": [ + { + "name": "{{ name }}", + "range": ["{{ range }}"], + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_rule_source_pool", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+rule + \s+(?P<id>\d+) + \s+source + \s+pool + \s+(?P<pool>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat rule {{ id }} source pool {{ pool }}", + "result": { + "nat": { + "cgnat": { + "rule": [ + { + "id": "{{ id }}", + "source": {"pool": "{{ pool }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "cgnat_rule_translation_pool", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+rule + \s+(?P<id>\d+) + \s+translation + \s+pool + \s+(?P<pool>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}", + "result": { + "nat": { + "cgnat": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"pool": "{{ pool }}"}, + }, + ], + }, + }, + }, + }, + + # ------------------------- + # GENERIC NAT + # ------------------------- + { + "name": "nat_type_description", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+description + \s+(?P<description>.+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} description '{{ description }}'", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "description": "{{ description }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_protocol", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+protocol + \s+(?P<protocol>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "protocol": "{{ protocol }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_disable", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+disable + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} disable", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "disable": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_exclude", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+exclude + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} exclude", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "exclude": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_log", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+log + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} log", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "log": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_prefix", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+prefix + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"prefix": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_fqdn", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+fqdn + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"fqdn": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address_group", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+group + \s+(?P<gtype>address-group|domain-group|mac-group|network-group|port-group) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": { + "{{ gtype | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_options", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+options + \s+(?P<opt>address-mapping|port-mapping) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "{{ opt | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_redirect", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+redirect + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "redirect_port": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_inbound_interface_name", + "getval": re.compile( + r""" + ^set + \s+nat + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat {{ type }} rule {{ id }} inbound-interface name {{ value }}", + "result": { + "nat": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_inbound_interface_group", + "getval": re.compile( + r""" + ^set + \s+nat + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+group + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat {{ type }} rule {{ id }} inbound-interface group {{ value }}", + "result": { + "nat": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"group": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_static_inbound_interface", + "getval": re.compile( + r""" + ^set + \s+nat + \s+static + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat static rule {{ id }} inbound-interface {{ value }}", + "result": { + "nat": { + "static": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat6x_inbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_outbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface name {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "outbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_outbound_interface_group", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+group + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface group {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "outbound_interface": {"group": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_packet_type", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+packet-type + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "packet_type": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_backend", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+backend + \s+(?P<ip>\S+) + \s+weight + \s+(?P<weight>\d+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance backend {{ ip }} weight {{ weight }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "load_balance": { + "backend": [ + {"ip": "{{ ip }}", "weight": "{{ weight }}"}, + ], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_hash", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+hash + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "load_balance": {"hash": ["{{ value }}"]}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_match_mark", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+match + \s+mark + \s+(?P<mark>\d+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} match mark {{ mark }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "match": {"mark": "{{ mark }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool", + "getval": re.compile(r"^$"), # never matches — setval only + "setval": _tmplt_nat64_translation_pool, + "result": {}, + }, + { + "name": "nat64_translation_pool_address", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} address {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "address": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + + { + "name": "nat64_translation_pool_description", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+description + \s+(?P<value>.+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} description '{{ value }}'", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "description": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_disable", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+disable + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} disable", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "disable": True}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_port", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} port {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "port": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_protocol", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+protocol + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} protocol {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "protocol": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py index e9d8a0cb..b83a3e1c 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py index 0d7eaf84..5183aec4 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -23,6 +22,7 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( get_interface_type, + get_interface_with_vif, ) @@ -36,21 +36,23 @@ def _get_parameters(data): def _tmplt_ospf_int_delete(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) - command = ( - "interfaces " + int_type + " {name} ".format(**config_data) + params[1] + " " + params[0] - ) + command = "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] return command def _tmplt_ospf_int_cost(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -62,11 +64,14 @@ def _tmplt_ospf_int_cost(config_data): def _tmplt_ospf_int_auth_password(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -79,11 +84,14 @@ def _tmplt_ospf_int_auth_password(config_data): def _tmplt_ospf_int_auth_md5(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -98,11 +106,14 @@ def _tmplt_ospf_int_auth_md5(config_data): def _tmplt_ospf_int_auth_md5_delete(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -114,11 +125,14 @@ def _tmplt_ospf_int_auth_md5_delete(config_data): def _tmplt_ospf_int_bw(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -130,11 +144,14 @@ def _tmplt_ospf_int_bw(config_data): def _tmplt_ospf_int_hello_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -146,11 +163,14 @@ def _tmplt_ospf_int_hello_interval(config_data): def _tmplt_ospf_int_dead_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -162,15 +182,10 @@ def _tmplt_ospf_int_dead_interval(config_data): def _tmplt_ospf_int_mtu_ignore(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( - "interfaces " - + int_type - + " {name} ".format(**config_data) - + params[1] - + " " - + params[0] - + " mtu-ignore" + "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] + " mtu-ignore" ) return command @@ -178,11 +193,14 @@ def _tmplt_ospf_int_mtu_ignore(config_data): def _tmplt_ospf_int_network(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -194,11 +212,14 @@ def _tmplt_ospf_int_network(config_data): def _tmplt_ospf_int_priority(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -210,11 +231,14 @@ def _tmplt_ospf_int_priority(config_data): def _tmplt_ospf_int_retransmit_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -226,11 +250,14 @@ def _tmplt_ospf_int_retransmit_interval(config_data): def _tmplt_ospf_int_transmit_delay(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -242,11 +269,14 @@ def _tmplt_ospf_int_transmit_delay(config_data): def _tmplt_ospf_int_ifmtu(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -258,11 +288,14 @@ def _tmplt_ospf_int_ifmtu(config_data): def _tmplt_ospf_int_instance(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -274,16 +307,9 @@ def _tmplt_ospf_int_instance(config_data): def _tmplt_ospf_int_passive(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) - command = ( - "interfaces " - + int_type - + " {name} ".format(**config_data) - + params[1] - + " " - + params[0] - + " passive" - ) + command = "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] + " passive" return command @@ -308,6 +334,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) *$""", @@ -316,7 +343,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "remval": _tmplt_ospf_int_delete, "compval": "address_family", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -332,18 +359,18 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+authentication \s+plaintext-password - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, "compval": "address_family.authentication", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -362,6 +389,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+authentication @@ -369,15 +397,14 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+key-id \s+(?P<id>\d+) \s+md5-key - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, "remval": _tmplt_ospf_int_auth_md5_delete, "compval": "address_family.authentication", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -399,6 +426,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+bandwidth @@ -409,7 +437,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_bw, "compval": "address_family.bandwidth", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -426,6 +454,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+cost @@ -436,7 +465,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_cost, "compval": "address_family.cost", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -453,6 +482,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+hello-interval @@ -463,7 +493,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_hello_interval, "compval": "address_family.hello_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -480,6 +510,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+dead-interval @@ -490,7 +521,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_dead_interval, "compval": "address_family.dead_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -507,6 +538,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+(?P<mtu>mtu-ignore) @@ -516,7 +548,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_mtu_ignore, "compval": "address_family.mtu_ignore", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -533,17 +565,17 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+network - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, "compval": "address_family.network", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -560,6 +592,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+priority @@ -570,7 +603,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_priority, "compval": "address_family.priority", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -587,6 +620,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+retransmit-interval @@ -597,7 +631,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_retransmit_interval, "compval": "address_family.retransmit_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -614,6 +648,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+transmit-delay @@ -624,7 +659,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_transmit_delay, "compval": "address_family.transmit_delay", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -641,6 +676,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+ifmtu @@ -651,7 +687,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_ifmtu, "compval": "address_family.ifmtu", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -668,6 +704,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+instance-id @@ -678,7 +715,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_instance, "compval": "address_family.instance", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -695,6 +732,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+(?P<pass>passive) @@ -704,7 +742,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_passive, "compval": "address_family.passive", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -721,12 +759,13 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? .*$""", re.VERBOSE, ), "setval": "set interface {{ type }} {{ name }}", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", }, }, ] diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py index 43fae1e9..8d09011f 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type """ @@ -31,9 +32,7 @@ def _get_parameters(data): def _tmplt_ospf_int_delete(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " + params[0] + " interface {name}".format(**config_data) - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) return command @@ -81,10 +80,7 @@ def _tmplt_ospf_int_auth_md5(config_data): def _tmplt_ospf_int_auth_md5_delete(config_data): params = _get_parameters(config_data["address_family"]) command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " authentication" + "protocols " + params[0] + " interface {name}".format(**config_data) + " authentication" ) return command @@ -128,12 +124,7 @@ def _tmplt_ospf_int_dead_interval(config_data): def _tmplt_ospf_int_mtu_ignore(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " mtu-ignore" - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) + " mtu-ignore" return command @@ -212,12 +203,7 @@ def _tmplt_ospf_int_instance(config_data): def _tmplt_ospf_int_passive(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " passive" - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) + " passive" return command @@ -239,8 +225,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+protocols \s+(?P<proto>ospf|ospfv3) \s+interface - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "remval": _tmplt_ospf_int_delete, @@ -265,8 +250,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+(?P<name>\S+) \s+authentication \s+plaintext-password - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, @@ -297,8 +281,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+key-id \s+(?P<id>\d+) \s+md5-key - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, @@ -458,8 +441,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+interface \s+(?P<name>\S+) \s+network - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, diff --git a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py index 0e99cfea..0e071199 100644 --- a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py +++ b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps.py b/plugins/module_utils/network/vyos/rm_templates/route_maps.py index 8f218a6b..fd8fdd9d 100644 --- a/plugins/module_utils/network/vyos/rm_templates/route_maps.py +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -33,8 +32,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "route_map", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$""", re.VERBOSE, ), "compval": "route_map", @@ -51,8 +49,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\s*$""", re.VERBOSE, ), "compval": "sequence", @@ -75,8 +72,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "call", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", @@ -99,8 +95,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "description", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", @@ -123,8 +118,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "action", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", @@ -147,8 +141,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "continue_sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", @@ -171,8 +164,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_next", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next)\s*$""", re.VERBOSE, ), "compval": "on_match.next", @@ -198,8 +190,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_goto", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+)\s*$""", re.VERBOSE, ), "compval": "on_match.goto", @@ -225,8 +216,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_ip", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.ip", @@ -254,8 +244,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_as", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.as", @@ -283,8 +272,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_as_path_exclude", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-exclude\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-exclude\s(?P<as>\S+)\s*$""", re.VERBOSE, ), "compval": "set.as_path_exclude", @@ -310,12 +298,12 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_as_path_prepend", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-prepend\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-prepend\s(?P<as>.*) + $""", re.VERBOSE, ), "compval": "set.as_path_prepend", - "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path-prepend {{set.as_path_prepend}}", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path-prepend '{{set.as_path_prepend}}'", "result": { "route_maps": { "{{ route_map }}": { @@ -337,10 +325,10 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_atomic_aggregate", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\satomic-aggregate(?P<as>) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\s(?P<as>atomic-aggregate)\s*$""", re.VERBOSE, ), + "compval": "set.atomic_aggregate", "setval": "policy route-map {{route_map}} rule {{sequence}} set atomic-aggregate", "result": { "route_maps": { @@ -363,8 +351,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_bgp_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+)\s*$""", re.VERBOSE, ), "compval": "set.bgp_extcommunity_rt", @@ -391,13 +378,12 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\scomm-list\s(?P<comm_list>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<comm_list>\S+)\s*$""", re.VERBOSE, ), - "compval": "set.comm_list.comm_list", + "compval": "match.community.community_list", "setval": "policy route-map {{route_map}} rule {{sequence}} " - "set comm-list comm-list {{set.comm_list.comm_list}}", + "match community community-list {{set.comm_list.comm_list}}", "result": { "route_maps": { "{{ route_map }}": { @@ -406,8 +392,8 @@ class Route_mapsTemplate(NetworkTemplate): "{{sequence}}": { "sequence": "{{sequence}}", - "set": { - "comm_list": {"comm_list": "{{comm_list}}"}, + "match": { + "community": {"community_list": "{{comm_list}}"}, }, }, }, @@ -419,8 +405,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list_delete", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\sdelete(?P<delete>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\s(?P<delete>delete)\s*$""", re.VERBOSE, ), "compval": "set.comm_list.comm_list", @@ -447,8 +432,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-rt\s(?P<extcommunity_rt>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-rt\s(?P<extcommunity_rt>\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_rt", @@ -475,8 +459,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_soo", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-soo\s(?P<extcommunity_soo>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-soo\s(?P<extcommunity_soo>\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_soo", @@ -500,11 +483,65 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "set_extcommunity_bandwidth", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\sbandwidth\s(?P<extcommunity_bw>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth {{set.extcommunity_bandwidth}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth": "{{extcommunity_bw}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth_non_transitive", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) + \sset\sextcommunity\s(?P<extcommunity_bw_nt>bandwidth-non-transitive)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth_non_transitive", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth-non-transitive", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth_non_transitive": "{{True if extcommunity_bw_nt is defined}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "set_ip_next_hop", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+)\s*$""", re.VERBOSE, ), "compval": "set.ip_next_hop", @@ -533,8 +570,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sipv6-next-hop \s(?P<type>global|local) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "set.ipv6_next_hop", @@ -564,8 +600,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_large_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<large_community>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<large_community>\S+)\s*$""", re.VERBOSE, ), "compval": "set.large_community", @@ -592,8 +627,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_local_preference", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+)\s*$""", re.VERBOSE, ), "compval": "set.local_preference", @@ -620,8 +654,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric", @@ -648,8 +681,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric_type", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric_type", @@ -676,8 +708,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+)\s*$""", re.VERBOSE, ), "compval": "set.origin", @@ -704,8 +735,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_originator_id", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+)\s*$""", re.VERBOSE, ), "compval": "set.originator_id", @@ -732,8 +762,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_src", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+)\s*$""", re.VERBOSE, ), "compval": "set.src", @@ -760,8 +789,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_tag", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+)\s*$""", re.VERBOSE, ), "compval": "set.tag", @@ -788,8 +816,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_weight", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -813,11 +840,37 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "set_table", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stable\s(?P<table>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set table {{set.table}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "table": "{{table}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "set_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<value>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "set.community.value", @@ -846,8 +899,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_as_path", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+)\s*$""", re.VERBOSE, ), "compval": "match.as_path", @@ -874,8 +926,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -902,8 +953,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_exact_match", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>)\s*$""", re.VERBOSE, ), "compval": "match.community.exact_match", @@ -930,8 +980,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_extcommunity", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+)\s*$""", re.VERBOSE, ), "compval": "match.extcommunity", @@ -958,8 +1007,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_interface", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+)\s*$""", re.VERBOSE, ), "compval": "match.interface", @@ -986,8 +1034,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_large_community_large_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+)\s*$""", re.VERBOSE, ), "compval": "match.large_community_large_community_list", @@ -1014,8 +1061,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+)\s*$""", re.VERBOSE, ), "compval": "match.metric", @@ -1042,8 +1088,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+)\s*$""", re.VERBOSE, ), "compval": "match.origin", @@ -1070,8 +1115,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_peer", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+)\s*$""", re.VERBOSE, ), "compval": "match.peer", @@ -1100,8 +1144,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\saddress \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.address", @@ -1134,8 +1177,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\snexthop \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.next_hop", @@ -1168,8 +1210,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\sroute-source \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.route_source", @@ -1202,8 +1243,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\saddress \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.address", @@ -1235,8 +1275,7 @@ class Route_mapsTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\snexthop - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.next_hop", @@ -1261,12 +1300,37 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "match_protocol", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sprotocol\s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.protocol", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match protocol {{match.protocol}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "protocol": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "match_rpki", "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\srpki - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.rpki", diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py new file mode 100644 index 00000000..cf2d6b67 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py @@ -0,0 +1,1363 @@ +# -*- 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 Route_maps 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, +) + + +class Route_mapsTemplate14(NetworkTemplate): + def __init__(self, lines=None): + prefix = {"set": "set", "remove": "delete"} + super(Route_mapsTemplate14, self).__init__(lines=lines, tmplt=self, prefix=prefix) + + # fmt: off + PARSERS = [ + { + "name": "route_map", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "route_map", + "setval": "policy route-map {{route_map}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + }, + }, + }, + }, + { + "name": "sequence", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) + *$""", + re.VERBOSE, + ), + "compval": "sequence", + "setval": "policy route-map {{route_map}} rule {{sequence}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + }, + }, + }, + }, + }, + }, + { + "name": "call", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "call": "{{call}}", + }, + }, + }, + }, + }, + }, + { + "name": "description", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "description": "{{description}}", + }, + }, + }, + }, + }, + }, + { + "name": "action", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "action": "{{action}}", + }, + }, + }, + }, + }, + }, + { + "name": "continue_sequence", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "continue_sequence": "{{continue}}", + }, + }, + }, + }, + }, + }, + { + "name": "on_match_next", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next) + *$""", + re.VERBOSE, + ), + "compval": "on_match.next", + "setval": "policy route-map {{route_map}} rule {{sequence}} on-match next", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "on_match": { + "next": "{{True if next is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "on_match_goto", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "on_match.goto", + "setval": "policy route-map {{route_map}} rule {{sequence}} on-match goto {{on_match.goto}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "on_match": { + "goto": "{{goto}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_aggregator_ip", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.aggregator.ip", + "setval": "policy route-map {{route_map}} rule {{sequence}} set aggregator ip {{set.aggregator.ip}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "aggregator": { + "ip": "{{ip}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_aggregator_as", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.aggregator.as", + "setval": "policy route-map {{route_map}} rule {{sequence}} set aggregator as {{set.aggregator.as}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "aggregator": { + "as": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_as_path_exclude", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path\sexclude\s(?P<as>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.as_path_exclude", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path exclude {{set.as_path_exclude}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "as_path_exclude": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_as_path_prepend", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path\sprepend\s(?P<as>.*) + $""", + re.VERBOSE, + ), + "compval": "set.as_path_prepend", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path prepend '{{set.as_path_prepend}}'", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "as_path_prepend": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_atomic_aggregate", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\s(?P<as>atomic-aggregate) + *$""", + re.VERBOSE, + ), + "compval": "set.atomic_aggregate", + "setval": "policy route-map {{route_map}} rule {{sequence}} set atomic-aggregate", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "atomic_aggregate": "{{True if as is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_bgp_extcommunity_rt", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.bgp_extcommunity_rt", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set bgp-extcommunity-rt {{set.bgp_extcommunity_rt}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "bgp_extcommunity_rt": "{{bgp}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_comm_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<comm_list>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.community.community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community community-list {{set.comm_list.comm_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"community_list": "{{comm_list}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_comm_list_delete", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\s(?P<delete>delete)\s*$""", + re.VERBOSE, + ), + "compval": "set.comm_list.comm_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set comm-list delete", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "comm_list": {"delete": "{{True if delete is defined}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_rt", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\srt\s(?P<extcommunity_rt>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_rt", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity rt {{set.extcommunity_rt}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_rt": "{{extcommunity_rt}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_soo", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\ssoo\s(?P<extcommunity_soo>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_soo", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity soo {{set.extcommunity_soo}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_soo": "{{extcommunity_soo}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\sbandwidth\s(?P<extcommunity_bw>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth {{set.extcommunity_bandwidth}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth": "{{extcommunity_bw}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth_non_transitive", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\s(?P<extcommunity_bw_nt>bandwidth-non-transitive) + *$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth_non_transitive", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth-non-transitive", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth_non_transitive": "{{True if extcommunity_bw_nt is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_ip_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.ip_next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set ip-next-hop {{set.ip_next_hop}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "ip_next_hop": "{{ip_next_hop}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_ipv6_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sipv6-next-hop + \s(?P<type>global|local) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.ipv6_next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set ipv6-next-hop {{set.ipv6_next_hop.ip_type}} {{set.ipv6_next_hop.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "ipv6_next_hop": { + "ip_type": "{{type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_large_community", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<op>none|replace\s(?P<large_community>\S+)) + $""", + re.VERBOSE, + ), + "compval": "set.large_community", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set large-community {{set.large_community if set.large_community == 'none' else 'replace ' + set.large_community}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "large_community": "{{op if op == 'none' else large_community}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_local_preference", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.local_preference", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set local-preference {{set.local_preference}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "local_preference": "{{local_preference}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_metric", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.metric", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set metric {{set.metric}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "metric": "{{metric}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_metric_type", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.metric_type", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set metric-type {{set.metric_type}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "metric_type": "{{metric_type}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_origin", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.origin", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set origin {{set.origin}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "origin": "{{origin}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_originator_id", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.originator_id", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set originator-id {{set.originator_id}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "originator_id": "{{originator_id}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_src", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.src", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set src {{set.src}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "src": "{{src}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_tag", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.tag", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set tag {{set.tag}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "tag": "{{tag}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_weight", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set weight {{set.weight}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "weight": "{{weight}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_table", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stable\s(?P<table>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set table {{set.table}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "table": "{{table}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_community", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<op>none|replace\s(?P<value>\S+)) + $""", + re.VERBOSE, + ), + "compval": "set.community.value", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set community {{set.community.value if set.community.value == 'none' else 'replace ' + set.community.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "community": { + "value": "{{op if op == 'none' else value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_as_path", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.as_path", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match as-path {{match.as_path}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "as_path": "{{as_path}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_community_community_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.community.community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community community-list {{match.community.community_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"community_list": "{{community_list}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_community_exact_match", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>) + *$""", + re.VERBOSE, + ), + "compval": "match.community.exact_match", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community exact-match", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"exact_match": "{{True if exact_match is defined}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_extcommunity", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.extcommunity", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match extcommunity {{match.extcommunity}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "extcommunity": "{{extcommunity}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_interface", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.interface", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match interface {{match.interface}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "interface": "{{interface}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_large_community_large_community_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.large_community_large_community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match large-community large-community-list {{match.large_community_large_community_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "large_community_large_community_list": "{{lc}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_metric", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.metric", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match metric {{match.metric}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "metric": "{{metric}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_origin", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.origin", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match origin {{match.origin}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "origin": "{{origin}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_peer", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.peer", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match peer {{match.peer}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "peer": "{{peer}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_address", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\saddress + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.address", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip address {{match.ip.address.list_type}} {{match.ip.address.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "address": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\snexthop + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip nexthop {{match.ip.next_hop.list_type}} {{match.ip.next_hop.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "next_hop": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_route_source", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\sroute-source + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.route_source", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip route-source {{match.ip.route_source.list_type}} {{match.ip.route_source.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "route_source": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ipv6_address", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\saddress + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ipv6.address", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ipv6 address {{match.ipv6.address.list_type}} {{match.ipv6.address.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ipv6": { + "address": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ipv6_nexthop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\snexthop + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ipv6.next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ipv6 nexthop {{match.ipv6.next_hop}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ipv6": { + "next_hop": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_protocol", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sprotocol\s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.protocol", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match protocol {{match.protocol}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "protocol": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_rpki", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\srpki + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.rpki", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match rpki {{match.rpki}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "rpki": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py index 71753083..bd76a5ae 100644 --- a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py +++ b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -140,9 +139,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\scommunity \s+(?P<name>\S+) - \s*(?P<auth>authorization\srw|authorization\sro)* - \s*(client\s(?P<client>\S+))* - \s*(network\s(?P<network>\S+))* + \s*(?P<auth>authorization\srw|authorization\sro)? + \s*(client\s(?P<client>\S+))? + \s*(network\s(?P<network>\S+))? $""", re.VERBOSE, ), @@ -164,8 +163,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\scontact - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp contact {{ contact }}", @@ -179,8 +177,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sdescription - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp description {{ description }}", @@ -195,8 +192,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\slisten-address \s+(?P<addr>\S+) - \s*(port)* - \s*(?P<port>\d+)* + \s*(port)? + \s*(?P<port>\d+)? $""", re.VERBOSE, ), @@ -232,8 +229,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\ssmux-peer - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp smux-peer {{ smux_peer }}", @@ -247,8 +243,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\strap-source - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp trap-source {{ trap_source }}", @@ -263,9 +258,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\strap-target \s+(?P<name>\S+) - \s*(?P<comm>community\s\S+)* - \s*(?P<port>port\s\d+)* - $""", + \s*(?P<comm>community\s\S+)? + \s*(?P<port>port\s\d+)? $""", re.VERBOSE, ), "setval": _tmplt_snmp_server_trap_target, @@ -283,8 +277,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sv3\sengineid - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp v3 engineid {{ snmp_v3.engine_id }}", @@ -301,9 +294,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\sgroup \s+(?P<name>\S+) - \s*(?P<mode>mode\s\S+)* - \s*(?P<sec>seclevel\s\S+)* - \s*(?P<view>view\s\S+)* + \s*(?P<mode>mode\s\S+)? + \s*(?P<sec>seclevel\s\S+)? + \s*(?P<view>view\s\S+)? $""", re.VERBOSE, ), @@ -329,9 +322,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) \s+auth - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -358,8 +351,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<port>port\s\d+)* - $""", + \s+(?P<port>port\s\d+)? $""", re.VERBOSE, ), "setval": "service snmp v3 trap-target port {{ snmp_v3.trap_targets.port }}", @@ -381,7 +373,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<protocol>protocol\s\S+)* + \s+(?P<protocol>protocol\s\S+)? $""", re.VERBOSE, ), @@ -404,7 +396,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<type>type\s\S+)* + \s+(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -427,7 +419,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<user>user\s\S+)* + \s+(?P<user>user\s\S+)? $""", re.VERBOSE, ), @@ -451,9 +443,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) \s+privacy - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -481,9 +473,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) \s+auth - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -511,9 +503,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) \s+privacy - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -540,8 +532,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) - \s+(?P<group>group\s.+)* - $""", + \s+(?P<group>group\s.+)? $""", re.VERBOSE, ), "setval": "service snmp v3 user {{ snmp_v3.users.user }} group {{ snmp_v3.users.group }}", @@ -563,7 +554,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) - \s+(?P<mode>mode\s\S+)* + \s+(?P<mode>mode\s\S+)? $""", re.VERBOSE, ), @@ -587,8 +578,8 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\sview \s+(?P<name>\S+) \s+(?P<oid>oid\s\S+) - \s*(?P<ex>exclude\s\S+)* - \s*(?P<mask>mask\s\S+)* + \s*(?P<ex>exclude\s\S+)? + \s*(?P<mask>mask\s\S+)? $""", re.VERBOSE, ), diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py new file mode 100644 index 00000000..ff889526 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py @@ -0,0 +1,997 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 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 VPN IPSEC 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, +) + + +class Vpn_ipsecTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Vpn_ipsecTemplate, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + # --------------------------------------------------------------- + # esp-group + # --------------------------------------------------------------- + { + "name": "esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + }, + }, + }, + }, + { + "name": "esp_group.proposal", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + }, + }, + }, + }, + }, + }, + { + "name": "esp_group.proposal.encryption", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sencryption\s'?(?P<encryption>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "encryption": "{{ encryption }}", + }, + }, + }, + }, + }, + }, + { + "name": "esp_group.proposal.hash", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \shash\s'?(?P<hash>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "hash": "{{ hash }}", + }, + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # ike-group + # --------------------------------------------------------------- + { + "name": "ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + }, + }, + }, + }, + { + "name": "ike_group.key_exchange", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \skey-exchange\s'?(?P<key_exchange>\w+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} key-exchange {{ key_exchange }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "key_exchange": "{{ key_exchange }}", + }, + }, + }, + }, + { + "name": "ike_group.proposal", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.dh_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sdh-group\s'?(?P<dh_group>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} dh-group {{ dh_group }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "dh_group": "{{ dh_group }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.encryption", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sencryption\s'?(?P<encryption>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "encryption": "{{ encryption }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.hash", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \shash\s'?(?P<hash>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "hash": "{{ hash }}", + }, + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication psk + # --------------------------------------------------------------- + { + "name": "authentication.psk", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + }, + }, + }, + }, + }, + { + "name": "authentication.psk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \sid\s'?(?P<id>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} id {{ id }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "id": ["{{ id }}"], + }, + }, + }, + }, + }, + { + "name": "authentication.psk.secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \ssecret\s'?(?P<secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} secret '{{ secret }}'", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "secret": "{{ secret }}", + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # profile + # --------------------------------------------------------------- + { + "name": "profile", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + }, + }, + }, + }, + { + "name": "profile.authentication.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sauthentication\smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication mode {{ mode }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "authentication": { + "mode": "{{ mode }}", + }, + }, + }, + }, + }, + { + "name": "profile.authentication.pre_shared_secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sauthentication\spre-shared-secret\s'?(?P<pre_shared_secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ pre_shared_secret }}'", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "authentication": { + "pre_shared_secret": "{{ pre_shared_secret }}", + }, + }, + }, + }, + }, + { + "name": "profile.bind_tunnel", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sbind\stunnel\s'?(?P<bind_tunnel>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} bind tunnel {{ bind_tunnel }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "bind_tunnel": ["{{ bind_tunnel }}"], + }, + }, + }, + }, + { + "name": "profile.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sesp-group\s'?(?P<esp_group>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} esp-group {{ esp_group }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "esp_group": "{{ esp_group }}", + }, + }, + }, + }, + { + "name": "profile.ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sike-group\s'?(?P<ike_group>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} ike-group {{ ike_group }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "ike_group": "{{ ike_group }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # ike-group: remaining fields + # --------------------------------------------------------------- + { + "name": "ike_group.close_action", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sclose-action\s'?(?P<close_action>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} close-action {{ close_action }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "close_action": "{{ close_action }}", + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.action", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\saction\s'?(?P<action>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection action {{ action }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"action": "{{ action }}"}, + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.interval", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\sinterval\s'?(?P<interval>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection interval {{ interval }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"interval": "{{ interval }}"}, + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.timeout", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\stimeout\s'?(?P<timeout>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection timeout {{ timeout }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"timeout": "{{ timeout }}"}, + }, + }, + }, + }, + { + "name": "ike_group.disable_mobike", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdisable-mobike + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} disable-mobike", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "disable_mobike": True, + }, + }, + }, + }, + { + "name": "ike_group.ikev2_reauth", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sikev2-reauth + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} ikev2-reauth", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "ikev2_reauth": True, + }, + }, + }, + }, + { + "name": "ike_group.lifetime", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \slifetime\s'?(?P<lifetime>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} lifetime {{ lifetime }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "lifetime": "{{ lifetime }}", + }, + }, + }, + }, + { + "name": "ike_group.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} mode {{ mode }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "mode": "{{ mode }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # esp-group: remaining fields + # --------------------------------------------------------------- + { + "name": "esp_group.compression", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \scompression + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} compression", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "compression": True, + }, + }, + }, + }, + { + "name": "esp_group.disable_rekey", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sdisable-rekey + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} disable-rekey", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "disable_rekey": True, + }, + }, + }, + }, + { + "name": "esp_group.life_bytes", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slife-bytes\s'?(?P<life_bytes>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} life-bytes {{ life_bytes }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "life_bytes": "{{ life_bytes }}", + }, + }, + }, + }, + { + "name": "esp_group.life_packets", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slife-packets\s'?(?P<life_packets>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} life-packets {{ life_packets }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "life_packets": "{{ life_packets }}", + }, + }, + }, + }, + { + "name": "esp_group.lifetime", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slifetime\s'?(?P<lifetime>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} lifetime {{ lifetime }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "lifetime": "{{ lifetime }}", + }, + }, + }, + }, + { + "name": "esp_group.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} mode {{ mode }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "mode": "{{ mode }}", + }, + }, + }, + }, + { + "name": "esp_group.pfs", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \spfs\s'?(?P<pfs>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} pfs {{ pfs }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "pfs": "{{ pfs }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication.psk: remaining fields + # --------------------------------------------------------------- + { + "name": "authentication.psk.secret_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \ssecret-type\s'?(?P<secret_type>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} secret-type {{ secret_type }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "secret_type": "{{ secret_type }}", + }, + }, + }, + }, + }, + { + "name": "authentication.psk.dhcp_interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \sdhcp-interface\s'?(?P<dhcp_interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} dhcp-interface {{ dhcp_interface }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "dhcp_interface": ["{{ dhcp_interface }}"], + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication.ppk + # --------------------------------------------------------------- + { + "name": "authentication.ppk", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \sid\s'?(?P<id>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} id {{ id }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "id": ["{{ id }}"], + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \ssecret\s'?(?P<secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} secret '{{ secret }}'", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "secret": "{{ secret }}", + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.secret_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \ssecret-type\s'?(?P<secret_type>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} secret-type {{ secret_type }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "secret_type": "{{ secret_type }}", + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # profile: remaining fields + # --------------------------------------------------------------- + { + "name": "profile.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sdisable + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} disable", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "disable": True, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # top-level: interface, log, options, disable_uniqreqids + # --------------------------------------------------------------- + { + "name": "interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sinterface\s'?(?P<interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec interface {{ interface }}", + "result": { + "interface": ["{{ interface }}"], + }, + }, + { + "name": "log.level", + "getval": re.compile( + r""" + ^set\svpn\sipsec\slog\slevel\s'?(?P<level>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec log level {{ level }}", + "result": { + "log": {"level": "{{ level }}"}, + }, + }, + { + "name": "log.subsystem", + "getval": re.compile( + r""" + ^set\svpn\sipsec\slog\ssubsystem\s'?(?P<subsystem>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec log subsystem {{ subsystem }}", + "result": { + "log": {"subsystem": ["{{ subsystem }}"]}, + }, + }, + { + "name": "options.disable_route_autoinstall", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sdisable-route-autoinstall + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options disable-route-autoinstall", + "result": { + "options": {"disable_route_autoinstall": True}, + }, + }, + { + "name": "options.flexvpn", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sflexvpn + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options flexvpn", + "result": { + "options": {"flexvpn": True}, + }, + }, + { + "name": "options.interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sinterface\s'?(?P<interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options interface {{ interface }}", + "result": { + "options": {"interface": "{{ interface }}"}, + }, + }, + { + "name": "options.retransmission.attempts", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\sattempts\s'?(?P<attempts>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission attempts {{ attempts }}", + "result": { + "options": {"retransmission": {"attempts": "{{ attempts }}"}}, + }, + }, + { + "name": "options.retransmission.base", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\sbase\s'?(?P<base>[\d.]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission base {{ base }}", + "result": { + "options": {"retransmission": {"base": "{{ base }}"}}, + }, + }, + { + "name": "options.retransmission.timeout", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\stimeout\s'?(?P<timeout>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission timeout {{ timeout }}", + "result": { + "options": {"retransmission": {"timeout": "{{ timeout }}"}}, + }, + }, + { + "name": "options.virtual_ip", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\svirtual-ip + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options virtual-ip", + "result": { + "options": {"virtual_ip": True}, + }, + }, + { + "name": "disable_uniqreqids", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sdisable-uniqreqids + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec disable-uniqreqids", + "result": { + "disable_uniqreqids": True, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py new file mode 100644 index 00000000..4319019c --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py @@ -0,0 +1,670 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 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 + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Vpn_ipsec_s2sTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Vpn_ipsec_s2sTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + prefix=prefix, + ) + + # fmt: off + PARSERS = [ + { + "name": "peer", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }}", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + }, + }, + }, + }, + }, + { + "name": "peer.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} disable", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "disable": True}}}, + }, + }, + { + "name": "peer.authentication.local_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\slocal-id\s'(?P<local_id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication local-id '{{ local_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"local_id": "{{ local_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.remote_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sremote-id\s'(?P<remote_id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication remote-id '{{ remote_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"remote_id": "{{ remote_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\smode\s'(?P<mode>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication mode '{{ mode }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"mode": "{{ mode }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.use_x509_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\suse-x509-id$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication use-x509-id", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"use_x509_id": True}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sppk\sid\s'(?P<id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk id '{{ id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"id": "{{ id }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.required", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sppk\srequired$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk required", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"required": True}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.local_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\slocal-key\s'(?P<local_key>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa local-key '{{ local_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"local_key": "{{ local_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.remote_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\sremote-key\s'(?P<remote_key>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa remote-key '{{ remote_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"remote_key": "{{ remote_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\spassphrase\s'(?P<passphrase>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\scertificate\s'(?P<certificate>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 certificate '{{ certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"certificate": "{{ certificate }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\spassphrase\s'(?P<passphrase>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.ca_certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\sca-certificate\s'(?P<ca_certificate>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 ca-certificate '{{ ca_certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"ca_certificate": ["{{ ca_certificate }}"]}}}}, + }, + }, + }, + { + "name": "peer.childless", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \schildless\s'(?P<childless>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} childless '{{ childless }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "childless": "{{ childless }}"}}}, + }, + }, + { + "name": "peer.connection_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sconnection-type\s'(?P<connection_type>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} connection-type '{{ connection_type }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "connection_type": "{{ connection_type }}"}}}, + }, + }, + { + "name": "peer.default_esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdefault-esp-group\s'(?P<default_esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} default-esp-group '{{ default_esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "default_esp_group": "{{ default_esp_group }}"}}}, + }, + }, + { + "name": "peer.description", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdescription\s'(?P<description>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} description '{{ description }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "description": "{{ description }}"}}}, + }, + }, + { + "name": "peer.dhcp_interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdhcp-interface\s'(?P<dhcp_interface>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} dhcp-interface '{{ dhcp_interface }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "dhcp_interface": "{{ dhcp_interface }}"}}}, + }, + }, + { + "name": "peer.force_udp_encapsulation", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sforce-udp-encapsulation$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} force-udp-encapsulation", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "force_udp_encapsulation": True}}}, + }, + }, + { + "name": "peer.ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sike-group\s'(?P<ike_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ike-group '{{ ike_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ike_group": "{{ ike_group }}"}}}, + }, + }, + { + "name": "peer.ikev2_reauth", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sikev2-reauth\s'(?P<ikev2_reauth>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ikev2-reauth '{{ ikev2_reauth }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ikev2_reauth": "{{ ikev2_reauth }}"}}}, + }, + }, + { + "name": "peer.local_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \slocal-address\s'(?P<local_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} local-address '{{ local_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "local_address": "{{ local_address }}"}}}, + }, + }, + { + "name": "peer.remote_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sremote-address\s'(?P<remote_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} remote-address '{{ remote_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "remote_address": ["{{ remote_address }}"]}}}, + }, + }, + { + "name": "peer.replay_window", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sreplay-window\s'(?P<replay_window>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} replay-window '{{ replay_window }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "replay_window": "{{ replay_window }}"}}}, + }, + }, + { + "name": "peer.virtual_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svirtual-address\s'(?P<virtual_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} virtual-address '{{ virtual_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "virtual_address": ["{{ virtual_address }}"]}}}, + }, + }, + { + "name": "peer.tunnel", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }}", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} disable", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "disable": True, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sesp-group\s'(?P<esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} esp-group '{{ esp_group }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "esp_group": "{{ esp_group }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.protocol", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sprotocol\s'(?P<protocol>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} protocol '{{ protocol }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "protocol": "{{ protocol }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.priority", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\spriority\s'(?P<priority>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} priority '{{ priority }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "priority": "{{ priority }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.local.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\slocal\sport\s'(?P<port>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local port '{{ port }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "local": {"port": "{{ port }}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\slocal\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.remote.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sremote\sport\s'(?P<port>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote port '{{ port }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "remote": {"port": "{{ port }}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sremote\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.bind", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\sbind\s'(?P<bind>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti bind '{{ bind }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"bind": "{{ bind }}"}}}}, + }, + }, + { + "name": "peer.vti.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\sesp-group\s'(?P<esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti esp-group '{{ esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"esp_group": "{{ esp_group }}"}}}}, + }, + }, + { + "name": "peer.vti.traffic_selector.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\straffic-selector\slocal\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.traffic_selector.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\straffic-selector\sremote\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/vrf.py b/plugins/module_utils/network/vyos/rm_templates/vrf.py new file mode 100644 index 00000000..79928547 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vrf.py @@ -0,0 +1,248 @@ +# -*- 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 VRF 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, +) + + +class VrfTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(VrfTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module) + + # fmt: off + PARSERS = [ + { + "name": "table_id", + "getval": re.compile( + r""" + ^set + \s+vrf + \s+name + \s+(?P<name>\S+) + \s+table + \s+'(?P<tid>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{ name }} table {{ table_id }}", + "result": { + "name": "{{ name }}", + "table_id": "{{ tid }}", + }, + }, + { + "name": "bind_to_all", + "getval": re.compile( + r""" + ^set + \svrf + \s(?P<bta>bind-to-all) + $""", + re.VERBOSE, + ), + "setval": "vrf bind-to-all", + "result": { + "bind_to_all": "{{ True if bta is defined }}", + }, + }, + { + "name": "vni", + "getval": re.compile( + r""" + ^set + \s+vrf + \s+name + \s+(?P<name>\S+) + \s+vni + \s'(?P<vni>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} vni {{vni}}", + "result": { + "name": "{{ name }}", + "vni": "{{ vni }}", + }, + }, + { + "name": "description", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \sdescription + \s(?P<desc>\S+) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} description {{description}}", + "result": { + "name": "{{ name }}", + "description": "{{ desc }}", + }, + }, + { + "name": "disable_vrf", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} disable", + "compval": "disable", + "result": { + "name": "{{ name }}", + "disable": "{{ True if disable is defined }}", + }, + }, + # { + # "name": "address_family", + # "getval": re.compile( + # r""" + # ^set + # \svrf + # \sname + # \s(?P<name>\S+) + # \s(?P<af>ip|ipv6) + # $""", + # re.VERBOSE, + # ), + # "setval": "vrf name {{name}} {{ af }}", + # 'compval': "address_family", + # "result": { + # "name": "{{ name }}", + # "address_family": { + # '{{ "ipv4" if af == "ip" else "ipv6" }}': { + # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + # }, + # }, + # }, + # }, + # { + # "name": "address_family.disable_forwarding", + # "getval": re.compile( + # r""" + # ^set + # \svrf + # \sname + # \s(?P<name>\S+) + # \s(?P<af>ip|ipv6) + # \s(?P<df>disable-forwarding) + # $""", + # re.VERBOSE, + # ), + # "setval": "vrf name {{name}} {{ afi }} disable-forwarding", + # # "compval": "address_family.ipv6.disable_forwarding", + # "result": { + # "name": "{{ name }}", + # "address_family": { + # '{{ "ipv4" if af == "ip" else "ipv6" }}': { + # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + # "disable_forwarding": "{{ True if df is defined }}", + # }, + # }, + # }, + # }, + { + "name": "disable_forwarding", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \s(?P<df>disable-forwarding) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} disable-forwarding", + "compval": "disable_forwarding", + "result": { + "name": "{{ name }}", + 'address_family': [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "disable_forwarding": "{{ True if df is defined }}", + }], + }, + }, + { + "name": "disable_nht", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \snht + \s(?P<nht>no-resolve-via-default) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} nht no-resolve-via-default", + "compval": "nht_no_resolve_via_default", + "result": { + "name": "{{ name }}", + "address_family": [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "nht_no_resolve_via_default": "{{ True if nht is defined }}", + }], + }, + }, + { + "name": "route_maps", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \sprotocol + \s(?P<proto>\S+) + \sroute-map + \s'(?P<rm>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }} route-map {{ route_maps.rm_name }}", + "compval": "route_maps", + "remval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }}", + "result": { + "name": "{{ name }}", + "address_family": [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "route_maps": [{ + "rm_name": "{{ rm }}", + "protocol": "{{ proto }}", + }], + }], + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index 8722251e..89f12773 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -6,11 +6,8 @@ # utils from __future__ import absolute_import, division, print_function - __metaclass__ = type from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.six import iteritems - try: import ipaddress @@ -50,6 +47,18 @@ def get_interface_type(interface): return "dummy" +def get_interface_with_vif(interface): + """Gets virtual interface if any or return as is""" + vlan = None + interface_real = interface + if "." in interface: + interface_real, vlan = interface.split(".") + + if vlan is not None: + interface_real = interface_real + " vif " + vlan + return interface_real + + def dict_delete(base, comparable): """ This function generates a dict containing key, value pairs for keys @@ -186,7 +195,7 @@ def key_value_in_dict(have_key, have_value, want_dict): :param want_dict: :return: """ - for key, value in iteritems(want_dict): + for key, value in want_dict.items(): if key == have_key and value == have_value: return True return False @@ -258,9 +267,80 @@ def _is_w_same(w, h, key): def _in_target(h, key): """ - This function checks whether the target exist and key present in target config. + This functi checks whether the target exist and key present in target config. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h else False + + +def in_target_not_none(h, key): + """ + This function checks whether the target exist,key present in target config, and the value is not None. + :param h: target config. + :param key: attribute name. + :return: True/False. + """ + return True if h and key in h and h[key] is not None else False + + +def combine(a, b, recursive=False, list_merge="replace"): + """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") + + result = a.copy() + + for k, v in b.items(): + 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 + + return result diff --git a/plugins/module_utils/network/vyos/utils/version.py b/plugins/module_utils/network/vyos/utils/version.py index cc3028c3..6d84ef1c 100644 --- a/plugins/module_utils/network/vyos/utils/version.py +++ b/plugins/module_utils/network/vyos/utils/version.py @@ -7,6 +7,8 @@ """Provide version object to compare version numbers.""" from __future__ import absolute_import, division, print_function + + __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py index 1430b1b1..4983221e 100644 --- a/plugins/module_utils/network/vyos/vyos.py +++ b/plugins/module_utils/network/vyos/vyos.py @@ -34,6 +34,7 @@ import json from ansible.module_utils._text import to_text from ansible.module_utils.connection import Connection, ConnectionError + _DEVICE_CONFIGS = {} @@ -68,8 +69,13 @@ def get_config(module, flags=None, format=None): flags = [] if flags is None else flags global _DEVICE_CONFIGS - if _DEVICE_CONFIGS != {}: - return _DEVICE_CONFIGS + # If _DEVICE_CONFIGS is non-empty and module.params["match"] is "none", + # return the cached device configurations. This avoids redundant calls + # to the connection when no specific match criteria are provided. + if _DEVICE_CONFIGS != {} and ( + module.params["match"] is not None and module.params["match"] == "none" + ): + return to_text(_DEVICE_CONFIGS) else: connection = get_connection(module) try: @@ -81,6 +87,30 @@ def get_config(module, flags=None, format=None): return cfg +def copy_file(module, source, destination, proto="scp"): + """Copy a local file to the remote device over the existing network_cli + SSH session, using netcommon's generic connection-level file transfer + RPC (the same mechanism ansible.netcommon.net_put uses). + + Requires the device to have SCP/SFTP reachable over the same SSH + session used for network_cli. Mirrors the calling convention of + cisco.iosxr's module_utils copy_file(module, source, destination, proto), + confirmed against cisco.iosxr's iosxr_config.py call site: + copy_file(module, src, dst, "sftp"). + """ + connection = get_connection(module) + try: + timeout = connection.get_option("persistent_command_timeout") + connection.copy_file( + source=source, + destination=destination, + proto=proto, + timeout=timeout, + ) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + + def run_commands(module, commands, check_rc=True): connection = get_connection(module) try: @@ -90,11 +120,16 @@ def run_commands(module, commands, check_rc=True): return response -def load_config(module, commands, commit=False, comment=None): +def load_config(module, commands, commit=False, comment=None, confirm=None): connection = get_connection(module) try: - response = connection.edit_config(candidate=commands, commit=commit, comment=comment) + response = connection.edit_config( + candidate=commands, + commit=commit, + comment=comment, + confirm=confirm, + ) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) diff --git a/plugins/module_utils/network/vyos/vyos_file.py b/plugins/module_utils/network/vyos/vyos_file.py new file mode 100644 index 00000000..2b946c20 --- /dev/null +++ b/plugins/module_utils/network/vyos/vyos_file.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VyOS maintainers and contributors +# 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 + +import hashlib +import re + + +STAT_RE = re.compile(r"^(?P<mode>\d+)\s+(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)$") + + +def parse_stat(output): + """Parse `stat --format='%a %U %G %s' <path>` output. + Returns None if the path doesn't exist (caller checks rc/stderr first). + """ + m = STAT_RE.match(output.strip()) + if not m: + return None + d = m.groupdict() + return { + "mode": d["mode"].zfill(4)[-4:], + "owner": d["owner"], + "group": d["group"], + "size": int(d["size"]), + } + + +def _normalize_mode(mode): + if mode is None: + return None + return str(mode).zfill(4)[-4:] + + +def build_want(params, local_content_hash=None): + return { + "dest": params["dest"], + "state": params.get("state", "present"), + "owner": params.get("owner"), + "group": params.get("group"), + "mode": _normalize_mode(params.get("mode")), + "content_hash": local_content_hash, + } + + +def diff_want_have(want, have): + """Returns dict of {field: (have_val, want_val)} for fields that differ. + Identity is `dest`, not a config-tree path — this compares a stat-shaped + dict, not config lines. + """ + diff = {} + if want["state"] == "absent": + if have is not None: + diff["state"] = (have, "absent") + return diff + + if have is None: + diff["state"] = (None, "present") + for f in ("owner", "group", "mode"): + if want.get(f) is not None: + diff[f] = (None, want[f]) + if want.get("content_hash"): + diff["content"] = (None, want["content_hash"]) + return diff + + for f in ("owner", "group"): + if want.get(f) is not None and want[f] != have.get(f): + diff[f] = (have.get(f), want[f]) + + if want.get("mode") is not None: + want_mode = want["mode"] + have_mode = have.get("mode") + if want_mode[0] == "0": + # Caller didn't request specific setuid/setgid/sticky bits — + # don't fight VyOS's own conventions (e.g. /config/auth is + # deliberately setgid vyattacfg; see vyos.dev T2713). Compare + # only the rwx digits unless the caller explicitly asked for a + # non-zero leading digit. + if want_mode[-3:] != have_mode[-3:]: + diff["mode"] = (have_mode, want_mode) + elif want_mode != have_mode: + diff["mode"] = (have_mode, want_mode) + + if want.get("content_hash") and want["content_hash"] != have.get("content_hash"): + diff["content"] = (have.get("content_hash"), want["content_hash"]) + + return diff + + +def local_sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py index 6b1da84b..98a5b0ba 100644 --- a/plugins/modules/vyos_banner.py +++ b/plugins/modules/vyos_banner.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function - __metaclass__ = type # (c) 2017, Ansible by Red Hat, inc @@ -33,7 +32,7 @@ description: VyOS. It allows playbooks to add or remote banner text from the active running configuration. version_added: 1.0.0 notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: banner: diff --git a/plugins/modules/vyos_bgp_address_family.py b/plugins/modules/vyos_bgp_address_family.py index 14c3605d..4172643e 100644 --- a/plugins/modules/vyos_bgp_address_family.py +++ b/plugins/modules/vyos_bgp_address_family.py @@ -10,7 +10,6 @@ The module file for vyos_bgp_address_family from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -19,7 +18,8 @@ version_added: 1.0.0 short_description: BGP Address Family resource module description: - This module manages BGP address family configuration of interfaces on devices running VYOS. -- Tested against VYOS 1.3, 1.4 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 +- The provided examples of commands are valid for VyOS 1.4+ author: Gomathi Selvi Srinivasan (@GomathiselviS) options: config: @@ -285,17 +285,18 @@ EXAMPLES = """ # After State: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight '50' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight '50' # vyos@vyos:~$ # # Module Execution: @@ -363,17 +364,17 @@ EXAMPLES = """ # "before": {}, # "changed": true, # "commands": [ -# "set protocols bgp 100 address-family ipv4-unicast redistribute static metric 50", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number 4", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map map01", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export 10", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix 45", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map export map01", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map import map01", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight 50" +# "set protocols bgp address-family ipv4-unicast redistribute static metric 50", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number 4", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map map01", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export 10", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix 45", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map export map01", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map import map01", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight 50" # ], # @@ -382,17 +383,18 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight '50' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight '50' # vyos@vyos:~$ - name: Replace provided configuration with device configuration @@ -422,15 +424,16 @@ EXAMPLES = """ # After State: # # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export '10' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix '45' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast as-override +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export '10' # vyos@vyos:~$ # # @@ -549,39 +552,40 @@ EXAMPLES = """ # }, # "changed": true, # "commands": [ -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list", -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate", -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged", -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number 4", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast as-override", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map map01", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export 10", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix 45", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self" +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list", +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate", +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged", +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number 4", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast as-override", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map map01", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export 10", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix 45", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self" # ], # Using overridden # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast network 35.1.1.0/24 backdoor -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 address-family ipv6-unicast aggregate-address 6601:1:1:1::/64 summary-only -# set protocols bgp 100 address-family ipv6-unicast network 5001:1:1:1::/64 route-map 'map01' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export '10' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast network 35.1.1.0/24 backdoor +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp address-family ipv6-unicast aggregate-address 6601:1:1:1::/64 summary-only +# set protocols bgp address-family ipv6-unicast network 5001:1:1:1::/64 route-map 'map01' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast maximum-prefix '45' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast as-override +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast default-originate route-map 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast distribute-list export '10' # vyos@vyos:~$ - name: Override @@ -611,13 +615,14 @@ EXAMPLES = """ # After State # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only -# set protocols bgp 100 address-family ipv6-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast route-map import 'map01' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only +# set protocols bgp address-family ipv6-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix '45' +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast route-map import 'map01' # vyos@vyos:~$ @@ -742,21 +747,21 @@ EXAMPLES = """ # }, # "changed": true, # "commands": [ -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast distribute-list", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast default-originate", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast as-override", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast allowas-in", -# "delete protocols bgp 100 address-family ipv6 aggregate-address", -# "delete protocols bgp 100 address-family ipv6 network", -# "delete protocols bgp 100 address-family ipv4 network", -# "delete protocols bgp 100 address-family ipv4 redistribute", -# "set protocols bgp 100 address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only", -# "set protocols bgp 100 address-family ipv6-unicast redistribute static metric 50", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix 45", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast route-map import map01" +# "delete protocols bgp neighbor 20.33.1.1/24 address-family", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast distribute-list", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast default-originate", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast attribute-unchanged", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast as-override", +# "delete protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast allowas-in", +# "delete protocols bgp address-family ipv6 aggregate-address", +# "delete protocols bgp address-family ipv6 network", +# "delete protocols bgp address-family ipv4 network", +# "delete protocols bgp address-family ipv4 redistribute", +# "set protocols bgp address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only", +# "set protocols bgp address-family ipv6-unicast redistribute static metric 50", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix 45", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast route-map import map01" # ], # @@ -765,22 +770,23 @@ EXAMPLES = """ # Before State: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 address-family ipv6-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight '50' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix '45' -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self -# set protocols bgp 100 neighbor 100.11.34.12 address-family ipv6-unicast route-map import 'map01' +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast aggregate-address 60.9.2.0/24 summary-only +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp address-family ipv6-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map 'map01' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export '10' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix '45' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map export 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map import 'map01' +# set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight '50' +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast maximum-prefix '45' +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast nexthop-self +# set protocols bgp neighbor 100.11.34.12 address-family ipv6-unicast route-map import 'map01' # vyos@vyos:~$ - name: Delete @@ -800,11 +806,12 @@ EXAMPLES = """ # After State: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv6-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 100.11.34.12 +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv6-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 100.11.34.12 # vyos@vyos:~$ # # @@ -927,9 +934,9 @@ EXAMPLES = """ # }, # "changed": true, # "commands": [ -# "delete protocols bgp 100 address-family ipv4-unicast", -# "delete protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast", -# "delete protocols bgp 100 neighbor 100.11.34.12 address-family" +# "delete protocols bgp address-family ipv4-unicast", +# "delete protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast", +# "delete protocols bgp neighbor 100.11.34.12 address-family" # ], # @@ -1019,15 +1026,16 @@ EXAMPLES = """ # Native config: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 100 address-family ipv4-unicast network 35.1.1.0/24 backdoor -# set protocols bgp 100 address-family ipv4-unicast redistribute static metric '50' -# set protocols bgp 100 address-family ipv6-unicast aggregate-address 6601:1:1:1::/64 summary-only -# set protocols bgp 100 address-family ipv6-unicast network 5001:1:1:1::/64 route-map 'map01' -# set protocols bgp 100 address-family ipv6-unicast redistribute static metric '50' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override -# set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med -# set protocols bgp 100 neighbor 100.11.34.12 +# set protocols bgp system-as 100 +# set protocols bgp address-family ipv4-unicast network 35.1.1.0/24 backdoor +# set protocols bgp address-family ipv4-unicast redistribute static metric '50' +# set protocols bgp address-family ipv6-unicast aggregate-address 6601:1:1:1::/64 summary-only +# set protocols bgp address-family ipv6-unicast network 5001:1:1:1::/64 route-map 'map01' +# set protocols bgp address-family ipv6-unicast redistribute static metric '50' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number '4' +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override +# set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med +# set protocols bgp neighbor 100.11.34.12 - name: gather configs vyos.vyos.vyos_bgp_address_family: @@ -1131,17 +1139,17 @@ EXAMPLES = """ # Module Execution: # "rendered": [ -# "set protocols bgp 100 address-family ipv4-unicast redistribute static metric 50", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number 4", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map map01", -# "set protocols bgp 100 neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export 10", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix 45", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map export map01", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast route-map import map01", -# "set protocols bgp 100 neighbor 100.11.34.12 address-family ipv4-unicast weight 50" +# "set protocols bgp address-family ipv4-unicast redistribute static metric 50", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast allowas-in number 4", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast as-override", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv4-unicast attribute-unchanged med", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast default-originate route-map map01", +# "set protocols bgp neighbor 20.33.1.1/24 address-family ipv6-unicast distribute-list export 10", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast maximum-prefix 45", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast nexthop-self", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map export map01", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast route-map import map01", +# "set protocols bgp neighbor 100.11.34.12 address-family ipv4-unicast weight 50" # ] """ diff --git a/plugins/modules/vyos_bgp_global.py b/plugins/modules/vyos_bgp_global.py index 4d7db472..02f5e590 100644 --- a/plugins/modules/vyos_bgp_global.py +++ b/plugins/modules/vyos_bgp_global.py @@ -10,7 +10,6 @@ The module file for vyos_bgp_global from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -19,7 +18,8 @@ version_added: 1.0.0 short_description: BGP global resource module description: - This module manages BGP global configuration of interfaces on devices running VYOS. -- Tested against VYOS 1.3, 1.4 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 +- The provided examples of commands are valid for VyOS 1.4+ author: - Gomathi Selvi Srinivasan (@GomathiselviS) options: @@ -73,37 +73,6 @@ options: description: - Minimum interval for sending routing updates. type: int - # bfd: # <-- added in 1.3 - # description: Enable Bidirectional Forwarding Detection (BFD) support - # type: dict - # suboptions: - # check-control-plane-failure: - # description: - # - Allow to write CBIT independence in BFD outgoing packets - # and read both C-BIT value of BFD and lookup BGP peer status - # type: bool - # allowas_in: --> Moved to address-family before 1.3 - # description: - # - Number of occurrences of AS number. - # type: int - # as_override: --> Moved to address-family before 1.3 - # description: - # - AS for routes sent to this neighbor to be the local AS. - # type: bool - # attribute_unchanged: --> Moved to address-family before 1.3 - # description: - # - BGP attributes are sent unchanged. - # type: dict - # suboptions: - # as_path: - # description: as_path - # type: bool - # med: - # description: med - # type: bool - # next_hop: - # description: next_hop - # type: bool capability: description: - Advertise capabilities to this neighbor. @@ -117,13 +86,6 @@ options: description: - Advertise extended nexthop capability to this neighbor. type: bool - # orf: --> Removed before 1.3 - # description: - # - Advertise ORF capability to this neighbor. - # type: str - # choices: - # - send - # - receive default_originate: description: - Send default route to this neighbor @@ -145,70 +107,14 @@ options: - Disable sending community attributes to this neighbor. type: str choices: ['extended', 'standard'] - # distribute_list: --> Moved to address-family before 1.3 - # description: Access-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: Access-list to filter outgoing/incoming route updates to this neighbor - # type: str - # choices: ['export', 'import'] - # acl: - # description: Access-list number. - # type: int ebgp_multihop: description: - Allow this EBGP neighbor to not be on a directly connected network. Specify - the number hops. + the number of hops. type: int - # interface: # <-- added in 1.3 - # description: interface parameters - # type: dict - # suboptions: - # peer_group: - # description: Peer group for this neighbor - # type: str - # remote_as: - # description: - # - Remote AS number - # - Or 'external' for any number except this AS number - # - or 'internal' for this AS number - # type: str - # v6only: - # description: Enable BGP with v6 link-local only - # type: dict - # suboptions: - # peer_group: - # description: Peer group for this neighbor - # type: str - # remote_as: - # description: - # - Remote AS number - # - Or 'external' for any number except this AS number - # - or 'internal' for this AS number - # filter_list: --> Moved to address-family before 1.3 - # description: As-path-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # path_list: - # description: As-path-list to filter - # type: str local_as: description: local as number not to be prepended to updates from EBGP peers type: int - # maximum_prefix: --> Moved to address-family before 1.3 - # description: Maximum number of prefixes to accept from this neighbor - # nexthop-self Nexthop for routes sent to this neighbor to be the local router. - # type: int - # nexthop_self: --> Moved to address-family before 1.3 - # description: Nexthop for routes sent to this neighbor to be the local router. - # type: bool override_capability: description: Ignore capability negotiation with specified neighbor. type: bool @@ -227,61 +133,18 @@ options: port: description: Neighbor's BGP port type: int - # prefix_list: --> Moved to address-family before 1.3 - # description: Prefix-list to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # prefix_list: - # description: Prefix-list to filter - # type: str remote_as: description: Neighbor BGP AS number type: int - # remove_private_as: --> Moved to address-family before 1.3 - # description: Remove private AS numbers from AS path in outbound route updates - # type: bool - # route_map: --> Moved to address-family before 1.3 - # description: Route-map to filter route updates to/from this neighbor. - # type: list - # elements: dict - # suboptions: - # action: - # description: filter outgoing/incoming route updates - # type: str - # choices: ['export', 'import'] - # route_map: - # description: route-map to filter - # type: str - # route_reflector_client: --> Moved to address-family before 1.3 - # description: Neighbor as a route reflector client - # type: bool - # route_server_client: --> Removed prior to 1.3 - # description: Neighbor is route server client - # type: bool shutdown: description: Administratively shut down neighbor type: bool - # soft_reconfiguration: --> Moved to address-family before 1.3 - # description: Soft reconfiguration for neighbor - # type: bool solo: # <-- added in 1.3 description: Do not send back prefixes learned from the neighbor type: bool strict_capability_match: description: Enable strict capability negotiation type: bool - # unsuppress_map: --> Moved to address-family before 1.3 - # description: Route-map to selectively unsuppress suppressed routes - # type: str - - # weight: --> Moved to address-family before 1.3 - # description: Default weight for routes from this neighbor - # type: int timers: description: Neighbor timers type: dict @@ -539,33 +402,34 @@ EXAMPLES = """ # After State # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 aggregate-address 192.0.2.0/24 'summary-only' -# set protocols bgp 65536 aggregate-address 203.0.113.0/24 'as-set' -# set protocols bgp 65536 maximum-paths ebgp '20' -# set protocols bgp 65536 maximum-paths ibgp '55' -# set protocols bgp 65536 neighbor 192.0.2.25 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.25 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.25 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'as-path' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'med' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'next-hop' -# set protocols bgp 65536 neighbor 203.0.113.5 ebgp-multihop '2' -# set protocols bgp 65536 neighbor 203.0.113.5 remote-as '101' -# set protocols bgp 65536 neighbor 203.0.113.5 update-source '192.0.2.25' -# set protocols bgp 65536 neighbor 5001::64 distribute-list export '20' -# set protocols bgp 65536 neighbor 5001::64 distribute-list import '40' -# set protocols bgp 65536 neighbor 5001::64 maximum-prefix '34' -# set protocols bgp 65536 network 192.1.13.0/24 'backdoor' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters confederation identifier '66' -# set protocols bgp 65536 parameters confederation peers '20' -# set protocols bgp 65536 parameters confederation peers '55' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters router-id '192.1.2.9' -# set protocols bgp 65536 redistribute connected route-map 'map01' -# set protocols bgp 65536 redistribute kernel metric '45' -# set protocols bgp 65536 timers keepalive '35' +# set protocols bgp system-as 65536 +# set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' +# set protocols bgp aggregate-address 203.0.113.0/24 'as-set' +# set protocols bgp maximum-paths ebgp '20' +# set protocols bgp maximum-paths ibgp '55' +# set protocols bgp neighbor 192.0.2.25 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.25 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.25 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' +# set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' +# set protocols bgp neighbor 203.0.113.5 remote-as '101' +# set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' +# set protocols bgp neighbor 5001::64 distribute-list export '20' +# set protocols bgp neighbor 5001::64 distribute-list import '40' +# set protocols bgp neighbor 5001::64 maximum-prefix '34' +# set protocols bgp network 192.1.13.0/24 'backdoor' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters confederation identifier '66' +# set protocols bgp parameters confederation peers '20' +# set protocols bgp parameters confederation peers '55' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters router-id '192.1.2.9' +# set protocols bgp redistribute connected route-map 'map01' +# set protocols bgp redistribute kernel metric '45' +# set protocols bgp timers keepalive '35' # vyos@vyos:~$ # # # Module Execution: @@ -671,33 +535,33 @@ EXAMPLES = """ # "before": {}, # "changed": true, # "commands": [ -# "set protocols bgp 65536 neighbor 192.0.2.25 disable-connected-check", -# "set protocols bgp 65536 neighbor 192.0.2.25 timers holdtime 30", -# "set protocols bgp 65536 neighbor 192.0.2.25 timers keepalive 10", -# "set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged as-path", -# "set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged med", -# "set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged next-hop", -# "set protocols bgp 65536 neighbor 203.0.113.5 ebgp-multihop 2", -# "set protocols bgp 65536 neighbor 203.0.113.5 remote-as 101", -# "set protocols bgp 65536 neighbor 203.0.113.5 update-source 192.0.2.25", -# "set protocols bgp 65536 neighbor 5001::64 maximum-prefix 34", -# "set protocols bgp 65536 neighbor 5001::64 distribute-list export 20", -# "set protocols bgp 65536 neighbor 5001::64 distribute-list import 40", -# "set protocols bgp 65536 redistribute kernel metric 45", -# "set protocols bgp 65536 redistribute connected route-map map01", -# "set protocols bgp 65536 network 192.1.13.0/24 backdoor", -# "set protocols bgp 65536 aggregate-address 203.0.113.0/24 as-set", -# "set protocols bgp 65536 aggregate-address 192.0.2.0/24 summary-only", -# "set protocols bgp 65536 parameters bestpath as-path confed", -# "set protocols bgp 65536 parameters bestpath compare-routerid", -# "set protocols bgp 65536 parameters default no-ipv4-unicast", -# "set protocols bgp 65536 parameters router-id 192.1.2.9", -# "set protocols bgp 65536 parameters confederation peers 20", -# "set protocols bgp 65536 parameters confederation peers 55", -# "set protocols bgp 65536 parameters confederation identifier 66", -# "set protocols bgp 65536 maximum-paths ebgp 20", -# "set protocols bgp 65536 maximum-paths ibgp 55", -# "set protocols bgp 65536 timers keepalive 35" +# "set protocols bgp neighbor 192.0.2.25 disable-connected-check", +# "set protocols bgp neighbor 192.0.2.25 timers holdtime 30", +# "set protocols bgp neighbor 192.0.2.25 timers keepalive 10", +# "set protocols bgp neighbor 203.0.113.5 attribute-unchanged as-path", +# "set protocols bgp neighbor 203.0.113.5 attribute-unchanged med", +# "set protocols bgp neighbor 203.0.113.5 attribute-unchanged next-hop", +# "set protocols bgp neighbor 203.0.113.5 ebgp-multihop 2", +# "set protocols bgp neighbor 203.0.113.5 remote-as 101", +# "set protocols bgp neighbor 203.0.113.5 update-source 192.0.2.25", +# "set protocols bgp neighbor 5001::64 maximum-prefix 34", +# "set protocols bgp neighbor 5001::64 distribute-list export 20", +# "set protocols bgp neighbor 5001::64 distribute-list import 40", +# "set protocols bgp redistribute kernel metric 45", +# "set protocols bgp redistribute connected route-map map01", +# "set protocols bgp network 192.1.13.0/24 backdoor", +# "set protocols bgp aggregate-address 203.0.113.0/24 as-set", +# "set protocols bgp aggregate-address 192.0.2.0/24 summary-only", +# "set protocols bgp parameters bestpath as-path confed", +# "set protocols bgp parameters bestpath compare-routerid", +# "set protocols bgp parameters default no-ipv4-unicast", +# "set protocols bgp parameters router-id 192.1.2.9", +# "set protocols bgp parameters confederation peers 20", +# "set protocols bgp parameters confederation peers 55", +# "set protocols bgp parameters confederation identifier 66", +# "set protocols bgp maximum-paths ebgp 20", +# "set protocols bgp maximum-paths ibgp 55", +# "set protocols bgp timers keepalive 35" # ], # Using replaced: @@ -706,33 +570,34 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 aggregate-address 192.0.2.0/24 'summary-only' -# set protocols bgp 65536 aggregate-address 203.0.113.0/24 'as-set' -# set protocols bgp 65536 maximum-paths ebgp '20' -# set protocols bgp 65536 maximum-paths ibgp '55' -# set protocols bgp 65536 neighbor 192.0.2.25 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.25 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.25 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'as-path' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'med' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'next-hop' -# set protocols bgp 65536 neighbor 203.0.113.5 ebgp-multihop '2' -# set protocols bgp 65536 neighbor 203.0.113.5 remote-as '101' -# set protocols bgp 65536 neighbor 203.0.113.5 update-source '192.0.2.25' -# set protocols bgp 65536 neighbor 5001::64 distribute-list export '20' -# set protocols bgp 65536 neighbor 5001::64 distribute-list import '40' -# set protocols bgp 65536 neighbor 5001::64 maximum-prefix '34' -# set protocols bgp 65536 network 192.1.13.0/24 'backdoor' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters confederation identifier '66' -# set protocols bgp 65536 parameters confederation peers '20' -# set protocols bgp 65536 parameters confederation peers '55' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters router-id '192.1.2.9' -# set protocols bgp 65536 redistribute connected route-map 'map01' -# set protocols bgp 65536 redistribute kernel metric '45' -# set protocols bgp 65536 timers keepalive '35' +# set protocols bgp system-as 65536 +# set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' +# set protocols bgp aggregate-address 203.0.113.0/24 'as-set' +# set protocols bgp maximum-paths ebgp '20' +# set protocols bgp maximum-paths ibgp '55' +# set protocols bgp neighbor 192.0.2.25 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.25 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.25 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' +# set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' +# set protocols bgp neighbor 203.0.113.5 remote-as '101' +# set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' +# set protocols bgp neighbor 5001::64 distribute-list export '20' +# set protocols bgp neighbor 5001::64 distribute-list import '40' +# set protocols bgp neighbor 5001::64 maximum-prefix '34' +# set protocols bgp network 192.1.13.0/24 'backdoor' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters confederation identifier '66' +# set protocols bgp parameters confederation peers '20' +# set protocols bgp parameters confederation peers '55' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters router-id '192.1.2.9' +# set protocols bgp redistribute connected route-map 'map01' +# set protocols bgp redistribute kernel metric '45' +# set protocols bgp timers keepalive '35' # vyos@vyos:~$ - name: Replace @@ -757,11 +622,12 @@ EXAMPLES = """ # After state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 neighbor 192.0.2.40 advertisement-interval '72' -# set protocols bgp 65536 neighbor 192.0.2.40 capability orf prefix-list 'receive' -# set protocols bgp 65536 network 203.0.113.0/24 route-map 'map01' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 redistribute static route-map 'map01' +# set protocols bgp system-as 65536 +# set protocols bgp neighbor 192.0.2.40 advertisement-interval '72' +# set protocols bgp neighbor 192.0.2.40 capability orf prefix-list 'receive' +# set protocols bgp network 203.0.113.0/24 route-map 'map01' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ # # @@ -896,26 +762,26 @@ EXAMPLES = """ # }, # "changed": true, # "commands": [ -# "delete protocols bgp 65536 timers", -# "delete protocols bgp 65536 maximum-paths ", -# "delete protocols bgp 65536 maximum-paths ", -# "delete protocols bgp 65536 parameters router-id 192.1.2.9", -# "delete protocols bgp 65536 parameters default", -# "delete protocols bgp 65536 parameters confederation", -# "delete protocols bgp 65536 parameters bestpath compare-routerid", -# "delete protocols bgp 65536 aggregate-address", -# "delete protocols bgp 65536 network 192.1.13.0/24", -# "delete protocols bgp 65536 redistribute kernel", -# "delete protocols bgp 65536 redistribute kernel", -# "delete protocols bgp 65536 redistribute connected", -# "delete protocols bgp 65536 redistribute connected", -# "delete protocols bgp 65536 neighbor 5001::64", -# "delete protocols bgp 65536 neighbor 203.0.113.5", -# "delete protocols bgp 65536 neighbor 192.0.2.25", -# "set protocols bgp 65536 neighbor 192.0.2.40 advertisement-interval 72", -# "set protocols bgp 65536 neighbor 192.0.2.40 capability orf prefix-list receive", -# "set protocols bgp 65536 redistribute static route-map map01", -# "set protocols bgp 65536 network 203.0.113.0/24 route-map map01" +# "delete protocols bgp timers", +# "delete protocols bgp maximum-paths ", +# "delete protocols bgp maximum-paths ", +# "delete protocols bgp parameters router-id 192.1.2.9", +# "delete protocols bgp parameters default", +# "delete protocols bgp parameters confederation", +# "delete protocols bgp parameters bestpath compare-routerid", +# "delete protocols bgp aggregate-address", +# "delete protocols bgp network 192.1.13.0/24", +# "delete protocols bgp redistribute kernel", +# "delete protocols bgp redistribute kernel", +# "delete protocols bgp redistribute connected", +# "delete protocols bgp redistribute connected", +# "delete protocols bgp neighbor 5001::64", +# "delete protocols bgp neighbor 203.0.113.5", +# "delete protocols bgp neighbor 192.0.2.25", +# "set protocols bgp neighbor 192.0.2.40 advertisement-interval 72", +# "set protocols bgp neighbor 192.0.2.40 capability orf prefix-list receive", +# "set protocols bgp redistribute static route-map map01", +# "set protocols bgp network 203.0.113.0/24 route-map map01" # ], # Using deleted: @@ -924,11 +790,12 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 neighbor 192.0.2.40 advertisement-interval '72' -# set protocols bgp 65536 neighbor 192.0.2.40 capability orf prefix-list 'receive' -# set protocols bgp 65536 network 203.0.113.0/24 route-map 'map01' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 redistribute static route-map 'map01' +# set protocols bgp system-as 65536 +# set protocols bgp neighbor 192.0.2.40 advertisement-interval '72' +# set protocols bgp neighbor 192.0.2.40 capability orf prefix-list 'receive' +# set protocols bgp network 203.0.113.0/24 route-map 'map01' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ - name: Delete configuration @@ -980,10 +847,10 @@ EXAMPLES = """ # }, # "changed": true, # "commands": [ -# "delete protocols bgp 65536 neighbor 192.0.2.40", -# "delete protocols bgp 65536 redistribute", -# "delete protocols bgp 65536 network", -# "delete protocols bgp 65536 parameters" +# "delete protocols bgp neighbor 192.0.2.40", +# "delete protocols bgp redistribute", +# "delete protocols bgp network", +# "delete protocols bgp parameters" # ], # Using purged: @@ -991,33 +858,34 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 aggregate-address 192.0.2.0/24 'summary-only' -# set protocols bgp 65536 aggregate-address 203.0.113.0/24 'as-set' -# set protocols bgp 65536 maximum-paths ebgp '20' -# set protocols bgp 65536 maximum-paths ibgp '55' -# set protocols bgp 65536 neighbor 192.0.2.25 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.25 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.25 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'as-path' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'med' -# set protocols bgp 65536 neighbor 203.0.113.5 attribute-unchanged 'next-hop' -# set protocols bgp 65536 neighbor 203.0.113.5 ebgp-multihop '2' -# set protocols bgp 65536 neighbor 203.0.113.5 remote-as '101' -# set protocols bgp 65536 neighbor 203.0.113.5 update-source '192.0.2.25' -# set protocols bgp 65536 neighbor 5001::64 distribute-list export '20' -# set protocols bgp 65536 neighbor 5001::64 distribute-list import '40' -# set protocols bgp 65536 neighbor 5001::64 maximum-prefix '34' -# set protocols bgp 65536 network 192.1.13.0/24 'backdoor' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters confederation identifier '66' -# set protocols bgp 65536 parameters confederation peers '20' -# set protocols bgp 65536 parameters confederation peers '55' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters router-id '192.1.2.9' -# set protocols bgp 65536 redistribute connected route-map 'map01' -# set protocols bgp 65536 redistribute kernel metric '45' -# set protocols bgp 65536 timers keepalive '35' +# set protocols bgp system-as 65536 +# set protocols bgp aggregate-address 192.0.2.0/24 'summary-only' +# set protocols bgp aggregate-address 203.0.113.0/24 'as-set' +# set protocols bgp maximum-paths ebgp '20' +# set protocols bgp maximum-paths ibgp '55' +# set protocols bgp neighbor 192.0.2.25 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.25 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.25 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'as-path' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'med' +# set protocols bgp neighbor 203.0.113.5 attribute-unchanged 'next-hop' +# set protocols bgp neighbor 203.0.113.5 ebgp-multihop '2' +# set protocols bgp neighbor 203.0.113.5 remote-as '101' +# set protocols bgp neighbor 203.0.113.5 update-source '192.0.2.25' +# set protocols bgp neighbor 5001::64 distribute-list export '20' +# set protocols bgp neighbor 5001::64 distribute-list import '40' +# set protocols bgp neighbor 5001::64 maximum-prefix '34' +# set protocols bgp network 192.1.13.0/24 'backdoor' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters confederation identifier '66' +# set protocols bgp parameters confederation peers '20' +# set protocols bgp parameters confederation peers '55' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters router-id '192.1.2.9' +# set protocols bgp redistribute connected route-map 'map01' +# set protocols bgp redistribute kernel metric '45' +# set protocols bgp timers keepalive '35' # vyos@vyos:~$ @@ -1143,26 +1011,27 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 neighbor 192.0.2.43 advertisement-interval '72' -# set protocols bgp 65536 neighbor 192.0.2.43 capability 'dynamic' -# set protocols bgp 65536 neighbor 192.0.2.43 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.43 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.43 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.0 address-family 'ipv6-unicast' -# set protocols bgp 65536 neighbor 203.0.113.0 capability orf prefix-list 'receive' -# set protocols bgp 65536 network 203.0.113.0/24 route-map 'map01' -# set protocols bgp 65536 parameters 'always-compare-med' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters dampening half-life '33' -# set protocols bgp 65536 parameters dampening max-suppress-time '20' -# set protocols bgp 65536 parameters dampening re-use '60' -# set protocols bgp 65536 parameters dampening start-suppress-time '5' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters distance global external '66' -# set protocols bgp 65536 parameters distance global internal '20' -# set protocols bgp 65536 parameters distance global local '10' -# set protocols bgp 65536 redistribute static route-map 'map01' +# set protocols bgp system-as 65536 +# set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' +# set protocols bgp neighbor 192.0.2.43 capability 'dynamic' +# set protocols bgp neighbor 192.0.2.43 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.43 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.43 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' +# set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' +# set protocols bgp network 203.0.113.0/24 route-map 'map01' +# set protocols bgp parameters 'always-compare-med' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters dampening half-life '33' +# set protocols bgp parameters dampening max-suppress-time '20' +# set protocols bgp parameters dampening re-use '60' +# set protocols bgp parameters dampening start-suppress-time '5' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters distance global external '66' +# set protocols bgp parameters distance global internal '20' +# set protocols bgp parameters distance global local '10' +# set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ ^C # vyos@vyos:~$ @@ -1199,26 +1068,27 @@ EXAMPLES = """ # Before state: # vyos@vyos:~$ show configuration commands | match "set protocols bgp" -# set protocols bgp 65536 neighbor 192.0.2.43 advertisement-interval '72' -# set protocols bgp 65536 neighbor 192.0.2.43 capability 'dynamic' -# set protocols bgp 65536 neighbor 192.0.2.43 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.43 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.43 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.0 address-family 'ipv6-unicast' -# set protocols bgp 65536 neighbor 203.0.113.0 capability orf prefix-list 'receive' -# set protocols bgp 65536 network 203.0.113.0/24 route-map 'map01' -# set protocols bgp 65536 parameters 'always-compare-med' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters dampening half-life '33' -# set protocols bgp 65536 parameters dampening max-suppress-time '20' -# set protocols bgp 65536 parameters dampening re-use '60' -# set protocols bgp 65536 parameters dampening start-suppress-time '5' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters distance global external '66' -# set protocols bgp 65536 parameters distance global internal '20' -# set protocols bgp 65536 parameters distance global local '10' -# set protocols bgp 65536 redistribute static route-map 'map01' +# set protocols bgp system-as 65536 +# set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' +# set protocols bgp neighbor 192.0.2.43 capability 'dynamic' +# set protocols bgp neighbor 192.0.2.43 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.43 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.43 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' +# set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' +# set protocols bgp network 203.0.113.0/24 route-map 'map01' +# set protocols bgp parameters 'always-compare-med' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters dampening half-life '33' +# set protocols bgp parameters dampening max-suppress-time '20' +# set protocols bgp parameters dampening re-use '60' +# set protocols bgp parameters dampening start-suppress-time '5' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters distance global external '66' +# set protocols bgp parameters distance global internal '20' +# set protocols bgp parameters distance global local '10' +# set protocols bgp redistribute static route-map 'map01' # vyos@vyos:~$ ^C - name: gather configs @@ -1292,26 +1162,26 @@ EXAMPLES = """ # parsed.cfg -# set protocols bgp 65536 neighbor 192.0.2.43 advertisement-interval '72' -# set protocols bgp 65536 neighbor 192.0.2.43 capability 'dynamic' -# set protocols bgp 65536 neighbor 192.0.2.43 'disable-connected-check' -# set protocols bgp 65536 neighbor 192.0.2.43 timers holdtime '30' -# set protocols bgp 65536 neighbor 192.0.2.43 timers keepalive '10' -# set protocols bgp 65536 neighbor 203.0.113.0 address-family 'ipv6-unicast' -# set protocols bgp 65536 neighbor 203.0.113.0 capability orf prefix-list 'receive' -# set protocols bgp 65536 network 203.0.113.0/24 route-map 'map01' -# set protocols bgp 65536 parameters 'always-compare-med' -# set protocols bgp 65536 parameters bestpath as-path 'confed' -# set protocols bgp 65536 parameters bestpath 'compare-routerid' -# set protocols bgp 65536 parameters dampening half-life '33' -# set protocols bgp 65536 parameters dampening max-suppress-time '20' -# set protocols bgp 65536 parameters dampening re-use '60' -# set protocols bgp 65536 parameters dampening start-suppress-time '5' -# set protocols bgp 65536 parameters default 'no-ipv4-unicast' -# set protocols bgp 65536 parameters distance global external '66' -# set protocols bgp 65536 parameters distance global internal '20' -# set protocols bgp 65536 parameters distance global local '10' -# set protocols bgp 65536 redistribute static route-map 'map01' +# set protocols bgp neighbor 192.0.2.43 advertisement-interval '72' +# set protocols bgp neighbor 192.0.2.43 capability 'dynamic' +# set protocols bgp neighbor 192.0.2.43 'disable-connected-check' +# set protocols bgp neighbor 192.0.2.43 timers holdtime '30' +# set protocols bgp neighbor 192.0.2.43 timers keepalive '10' +# set protocols bgp neighbor 203.0.113.0 address-family 'ipv6-unicast' +# set protocols bgp neighbor 203.0.113.0 capability orf prefix-list 'receive' +# set protocols bgp network 203.0.113.0/24 route-map 'map01' +# set protocols bgp parameters 'always-compare-med' +# set protocols bgp parameters bestpath as-path 'confed' +# set protocols bgp parameters bestpath 'compare-routerid' +# set protocols bgp parameters dampening half-life '33' +# set protocols bgp parameters dampening max-suppress-time '20' +# set protocols bgp parameters dampening re-use '60' +# set protocols bgp parameters dampening start-suppress-time '5' +# set protocols bgp parameters default 'no-ipv4-unicast' +# set protocols bgp parameters distance global external '66' +# set protocols bgp parameters distance global internal '20' +# set protocols bgp parameters distance global local '10' +# set protocols bgp redistribute static route-map 'map01' - name: parse configs vyos.vyos.vyos_bgp_global: @@ -1430,25 +1300,25 @@ EXAMPLES = """ # Module Execution: # "rendered": [ -# "set protocols bgp 65536 neighbor 192.0.2.43 disable-connected-check", -# "set protocols bgp 65536 neighbor 192.0.2.43 advertisement-interval 72", -# "set protocols bgp 65536 neighbor 192.0.2.43 capability dynamic", -# "set protocols bgp 65536 neighbor 192.0.2.43 timers holdtime 30", -# "set protocols bgp 65536 neighbor 192.0.2.43 timers keepalive 10", -# "set protocols bgp 65536 neighbor 203.0.113.0 capability orf prefix-list receive", -# "set protocols bgp 65536 redistribute static route-map map01", -# "set protocols bgp 65536 network 203.0.113.0/24 route-map map01", -# "set protocols bgp 65536 parameters always-compare-med", -# "set protocols bgp 65536 parameters dampening half-life 33", -# "set protocols bgp 65536 parameters dampening max-suppress-time 20", -# "set protocols bgp 65536 parameters dampening re-use 60", -# "set protocols bgp 65536 parameters dampening start-suppress-time 5", -# "set protocols bgp 65536 parameters distance global internal 20", -# "set protocols bgp 65536 parameters distance global local 10", -# "set protocols bgp 65536 parameters distance global external 66", -# "set protocols bgp 65536 parameters bestpath as-path confed", -# "set protocols bgp 65536 parameters bestpath compare-routerid", -# "set protocols bgp 65536 parameters default no-ipv4-unicast" +# "set protocols bgp neighbor 192.0.2.43 disable-connected-check", +# "set protocols bgp neighbor 192.0.2.43 advertisement-interval 72", +# "set protocols bgp neighbor 192.0.2.43 capability dynamic", +# "set protocols bgp neighbor 192.0.2.43 timers holdtime 30", +# "set protocols bgp neighbor 192.0.2.43 timers keepalive 10", +# "set protocols bgp neighbor 203.0.113.0 capability orf prefix-list receive", +# "set protocols bgp redistribute static route-map map01", +# "set protocols bgp network 203.0.113.0/24 route-map map01", +# "set protocols bgp parameters always-compare-med", +# "set protocols bgp parameters dampening half-life 33", +# "set protocols bgp parameters dampening max-suppress-time 20", +# "set protocols bgp parameters dampening re-use 60", +# "set protocols bgp parameters dampening start-suppress-time 5", +# "set protocols bgp parameters distance global internal 20", +# "set protocols bgp parameters distance global local 10", +# "set protocols bgp parameters distance global external 66", +# "set protocols bgp parameters bestpath as-path confed", +# "set protocols bgp parameters bestpath compare-routerid", +# "set protocols bgp parameters default no-ipv4-unicast" # ] """ @@ -1472,17 +1342,17 @@ commands: returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) type: list sample: - - set protocols bgp 65536 redistribute static route-map map01 - - set protocols bgp 65536 network 203.0.113.0/24 route-map map01 - - set protocols bgp 65536 parameters always-compare-med + - set protocols bgp redistribute static route-map map01 + - set protocols bgp network 203.0.113.0/24 route-map map01 + - set protocols bgp parameters always-compare-med 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 protocols bgp 65536 redistribute static route-map map01 - - set protocols bgp 65536 network 203.0.113.0/24 route-map map01 - - set protocols bgp 65536 parameters always-compare-med + - set protocols bgp redistribute static route-map map01 + - set protocols bgp network 203.0.113.0/24 route-map map01 + - set protocols bgp parameters always-compare-med gathered: description: Facts about the network resource gathered from the remote device as structured data. returned: when I(state) is C(gathered) diff --git a/plugins/modules/vyos_command.py b/plugins/modules/vyos_command.py index bacbe26a..5131dd88 100644 --- a/plugins/modules/vyos_command.py +++ b/plugins/modules/vyos_command.py @@ -17,7 +17,6 @@ # from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -86,7 +85,7 @@ options: default: 1 type: int notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - Running C(show system boot-messages all) will cause the module to hang since VyOS is using a custom pager setting to display the output of that command. - If a command sent to the device requires answering a prompt, it is possible to pass diff --git a/plugins/modules/vyos_config.py b/plugins/modules/vyos_config.py index 60be02c8..53f8e043 100644 --- a/plugins/modules/vyos_config.py +++ b/plugins/modules/vyos_config.py @@ -34,10 +34,13 @@ version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). - To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they appear if present in the running configuration on device including the indentation. +- C(replace=config) currently has no way to scope its effect to part of the + configuration; it always operates against the entire device configuration. + There is no C(path) parameter to constrain it to a subtree. options: lines: description: @@ -46,6 +49,7 @@ options: device running-config to ensure idempotency and correct diff. Be sure to note the configuration command syntax as some commands are automatically modified by the device config parser. + - Not supported when C(replace) is set to C(config) -- see C(replace) below. type: list elements: str src: @@ -55,18 +59,38 @@ options: file can include Jinja2 template variables. The configuration lines in the source file should be similar to how it will appear if present in the running-configuration of the device including indentation to ensure idempotency and correct diff. + - When C(replace) is set to C(config), C(src) is required and must contain a + complete configuration in hierarchical/bracket format -- the same format + produced by C(show configuration) or found in C(/config/config.boot). Flat + C(set)/C(delete) command format (as produced by C(show configuration + commands)) is not accepted in that mode; VyOS's native C(load) command + rejects it with a parse error. type: path match: description: - The C(match) argument controls the method used to match against the current active configuration. By default, the desired config is matched against the active config and the deltas are loaded. If the C(match) argument is set to - C(none) the active configuration is ignored and the configuration is always - loaded. + C(none), the active configuration is ignored and the configuration is always + loaded. If the C(match) argument is set to C(enforce), the supplied C(lines) + or C(src) are treated as the complete desired end-state of the configuration, + rather than a set of deltas to apply. + C(enforce) enforces only the top-level configuration + sections present in the supplied candidate as complete end-states; + existing configuration within those sections but not mentioned in the + candidate is removed, so C(enforce) can generate C(delete) commands for + configuration the candidate does not mention. Top-level sections the + candidate does not reference at all are left completely untouched. + C(enforce) is intended for candidates made up of C(set) commands only; + supplying C(delete) lines alongside C(match=enforce) is not supported + and will raise an error. + - Ignored when C(replace) is set to C(config), since no line-level diff is + computed in that mode. type: str default: line choices: - line + - enforce - none backup: description: @@ -84,6 +108,28 @@ options: is ignored. default: configured by vyos_config type: str + confirm: + description: + - The C(confirm) argument will tell vyos to revert to the previous configuration + if not explicitly confirmed after applying the new config. When set to C(automatic) + this module will automatically confirm the configuration, if the current session + remains working with the new config. When set to C(manual), this module does + not issue the confirmation itself. + - Defaults to C(automatic) when C(match) is set to C(enforce), since C(enforce) + can generate C(delete) commands for configuration not mentioned in the + candidate and a bad commit should self-revert rather than leave the device + unreachable. Defaults to C(none) for all other C(match) values. + type: str + choices: + - automatic + - manual + - none + confirm_timeout: + description: + - Minutes to wait for confirmation before reverting the configuration. Does + not apply when C(confirm) is set to C(none) . + type: int + default: 10 config: description: - The C(config) argument specifies the base configuration to use to compare against @@ -92,12 +138,13 @@ options: The configuration lines in the option value should be similar to how it will appear if present in the running-configuration of the device including indentation to ensure idempotency and correct diff. + - Ignored when C(replace) is set to C(config). type: str save: description: - The C(save) argument controls whether or not changes made to the active configuration are saved to disk. This is independent of committing the config. When set - to True, the active configuration is saved. + to C(True), the active configuration is saved. type: bool default: no backup_options: @@ -123,6 +170,79 @@ options: in C(filename) within I(backup) directory. type: path type: dict + replace: + description: + - Controls how the module applies configuration to the device. + - When set to C(line) (default), the module computes a set/delete command + diff and pushes only the changed lines -- this is the existing behavior, + unchanged. + - When set to C(config), the module uploads the full candidate configuration + (C(src)) to the device and issues VyOS's native C(load) command in + configuration mode, which replaces the running configuration wholesale + with the candidate's exact contents. VyOS's own configuration engine + performs the reconciliation, rather than the module computing per-line + deltas. This mirrors the mechanism offered by C(cisco.iosxr.iosxr_config)'s + C(replace=config). + - C(replace=config) requires C(src) and does not accept C(lines) -- there is + no way to convert flat set/delete commands into the hierarchical form + C(load) requires without re-implementing VyOS's own config-tree builder. + - As with C(src) in the default C(line) mode, the module does not validate + the candidate's contents or format under C(replace=config) -- supplying a + well-formed, complete configuration is the caller's responsibility. + - C(replace=config) requires the device to accept file transfer (SCP) over + the same C(network_cli) SSH session used for configuration commands. + - C(replace=config) writes the candidate to a fixed path on the device + (overwritten on each run, matching C(cisco.iosxr.iosxr_config)'s own + C(replace=config) precedent). Running C(replace=config) concurrently + against the same host is not supported. + - Any configuration present on the device but omitted from the candidate + will be removed, including management interfaces, SSH access, and login + users if they are omitted. Always supply a complete configuration, never + a partial one. + - When capturing a candidate from the device's own output (for example + via C(show configuration)) rather than from a trusted, separately + maintained source, be aware that VyOS may return masked placeholder + values (for example a run of literal asterisks) in place of local + users' C(encrypted-password)/C(plaintext-password) values when queried + through automation, even though the identical command returns the real + value when typed interactively at a terminal. Pushing a masked capture + back through C(replace=config) sends the literal placeholder as the new + password value; VyOS's own commit-time validation is expected to reject + an obviously malformed hash, but a masked value that happens to pass + basic format validation could apply silently. Prefer sourcing + C(replace=config) candidates from a trusted, version-controlled + artifact rather than a live automated capture whenever the + configuration contains local password-based users. + - Even under C(check_mode), the candidate is written to a temporary file on + the device so that VyOS's own C(compare) can produce an accurate preview + diff. No C(commit) occurs in check mode. + - When combined with C(backup=yes), the value of C(changed) reflects + whether the backup file's content changed on the Ansible control node, + not whether the device configuration changed -- this is existing + behavior in the shared netcommon action plugin backing config-family + modules across collections, not specific to C(replace=config). + type: str + default: line + choices: + - line + - config + allow_password_change: + description: + - The C(allow_password_change) argument specifies whether any configuration lines which + would change a user's password should be filtered out. By default only plaintext + password changes are allowed and any encrypted-password keys are filtered out. In + order to allow all password updates, both plaintext and encrypted, set this argument + to C(all). + - Not applied when C(replace) is set to C(config); the candidate is loaded + as-is via VyOS's native C(load), which has no equivalent filtering + mechanism. + type: str + default: plaintext + choices: + - all + - plaintext + - encrypted + - none """ EXAMPLES = """ @@ -140,8 +260,14 @@ EXAMPLES = """ - name: render a Jinja2 template onto the VyOS router vyos.vyos.vyos_config: + match: enforce src: vyos_template.j2 +- name: revert after ten minutes, if connection is lost + vyos.vyos.vyos_config: + src: vyos_template.j2 + confirm: automatic + - name: for idempotency, use full-form commands vyos.vyos.vyos_config: lines: @@ -154,16 +280,40 @@ EXAMPLES = """ backup_options: filename: backup.cfg dir_path: /home/user + +- name: capture the complete hierarchical configuration for editing + # replace=config requires the complete desired configuration in + # hierarchical/bracket format -- never a partial one, and never flat + # set-command format. `backup: true` alone won't work here: it captures + # flat set-command output (via `show configuration commands`), which + # replace=config's underlying `load` command rejects. Capture the + # hierarchical form directly instead, edit it, then replace with the + # edited whole, as shown here. + vyos.vyos.vyos_command: + commands: "show configuration" + register: current_config + +- name: (edit current_config.stdout[0] as needed, save it locally, then) + vyos.vyos.vyos_config: + src: /home/user/edited_config.cfg + replace: config """ RETURN = """ commands: - description: The list of configuration commands sent to the device + description: + - In C(replace=line) mode (default), the list of set/delete commands sent to + the device. + - In C(replace=config) mode, contains only the single C(load <path>) command + actually issued to the device -- not an itemized diff. See C(diff) for the + actual change content, sourced from VyOS's own C(compare) output. returned: always type: list sample: ['...', '...'] filtered: - description: The list of configuration commands removed to avoid a load failure + description: + - The list of configuration commands removed to avoid a load failure. + - Not populated when C(replace) is set to C(config). returned: always type: list sample: ['...', '...'] @@ -193,13 +343,16 @@ time: type: str sample: "22:28:34" """ +import os import re +import tempfile -from ansible.module_utils._text import to_text +from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import ConnectionError from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( + copy_file, get_config, get_connection, load_config, @@ -209,9 +362,48 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import DEFAULT_COMMENT = "configured by vyos_config" -CONFIG_FILTERS = [ - re.compile(r"set system login user \S+ authentication encrypted-password"), -] +PASSWORD_NEEDLE = re.compile( + r"(?:set|delete) system login user \S+ authentication (encrypted|plaintext)-password", +) + +# diff_match=enforce's scoping can collapse an entire untouched subtree into +# a single parent delete (e.g. "delete system login" when a candidate +# touches system without restating login, or "delete system login user +# admin" without a specific authentication line). PASSWORD_NEEDLE can't see +# into a collapsed delete to know whether it removes a password -- since +# real users almost always have one configured, treat any subtree-level +# login deletion as password-bearing by default, same conservative stance +# as PASSWORD_NEEDLE itself. +LOGIN_SUBTREE_DELETE_NEEDLE = re.compile( + r"^delete system login(?:\s+user\s+\S+(?:\s+authentication)?)?\s*$", +) + + +def sanitize_config(config, result, allow): + result["filtered"] = list() + + if allow == "all": + return + + index_to_filter = list() + + for index, line in enumerate(list(config)): + found = PASSWORD_NEEDLE.search(line) + + if found is not None: + if allow == found[1]: + continue + result["filtered"].append(line) + index_to_filter.append(index) + continue + + if LOGIN_SUBTREE_DELETE_NEEDLE.match(line.strip()): + result["filtered"].append(line) + index_to_filter.append(index) + + # Delete all filtered configs + for filter_index in sorted(index_to_filter, reverse=True): + del config[filter_index] def get_candidate(module): @@ -270,22 +462,10 @@ def diff_config(commands, config): return list(updates) -def sanitize_config(config, result): - result["filtered"] = list() - index_to_filter = list() - for regex in CONFIG_FILTERS: - for index, line in enumerate(list(config)): - if regex.search(line): - result["filtered"].append(line) - index_to_filter.append(index) - # Delete all filtered configs - for filter_index in sorted(index_to_filter, reverse=True): - del config[filter_index] - - def run(module, result): # get the current active config from the node or passed in via # the config param + config = module.params["config"] or get_config(module) # create the candidate config object from the arguments @@ -303,16 +483,27 @@ def run(module, result): module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) commands = response.get("config_diff") - sanitize_config(commands, result) + + allow_password_change = module.params["allow_password_change"] + sanitize_config(commands, result, allow=allow_password_change) result["commands"] = commands + confirm_param = module.params["confirm"] + if confirm_param is None: + confirm_param = "automatic" if module.params["match"] == "enforce" else "none" + commit = not module.check_mode comment = module.params["comment"] + confirm = None + if confirm_param in ("automatic", "manual"): + confirm = module.params["confirm_timeout"] diff = None if commands: - diff = load_config(module, commands, commit=commit, comment=comment) + diff = load_config(module, commands, commit=commit, comment=comment, confirm=confirm) + if confirm_param == "automatic" and not module.check_mode: + run_commands(module, ["configure", "confirm", "exit"]) if result.get("filtered"): result["warnings"].append( @@ -325,24 +516,105 @@ def run(module, result): result["diff"] = {"prepared": diff} +def run_replace_config(module, result): + # replace=config: push the full candidate to the device and let VyOS's + # own `load` command perform the replacement natively, rather than + # computing a set/delete diff in Python. + # + # Deliberately smaller than cisco.iosxr's equivalent implementation: + # - No bidirectional pre-diff to decide whether anything changed -- + # confirmed on real VyOS 1.5 hardware that `load` of an + # already-applied file, followed by `compare`, natively reports + # "No changes between working and active configurations" with no + # Python-side pre-check needed. + # - No special `replace=<path>` argument threaded through load_config()/ + # edit_config() -- confirmed that `load <path>` behaves as an ordinary + # configuration command through the existing configure/compare/commit + # flow already implemented in Cliconf.edit_config(), unmodified. + # + # Candidate format requirement (hierarchical/bracket, not flat + # set/delete) is enforced by VyOS's own `load` parser, not by this + # module -- confirmed empirically: flat set-command input produces + # "ValueError: Failed to parse config: Syntax error...". + # module.params["src"] is already the rendered file *content* by this + # point, not a path -- netcommon's generic action plugin for src-based + # network config modules reads the local file and substitutes its + # (Jinja2-rendered) content into this param before the module runs. Same + # assumption get_candidate() already relies on elsewhere in this file. + candidate = to_bytes(module.params["src"], errors="surrogate_or_strict") + + tmp = tempfile.NamedTemporaryFile(delete=False) + local_path = tmp.name + try: + tmp.write(candidate) + tmp.close() + + # Fixed remote filename, always overwritten -- same precedent as + # cisco.iosxr.iosxr_config's copy_file_to_node(), which always + # writes to the same "/harddisk:/ansible_config.txt". Avoids + # per-run temp-file accumulation on the device, at the accepted + # cost (shared with iosxr_config) that two concurrent replace=config + # runs against the same host could race on this path. + remote_path = "/tmp/ansible_vyos_replace.cfg" + copy_file(module, local_path, remote_path, "scp") + finally: + os.unlink(local_path) + + confirm_param = module.params["confirm"] + if confirm_param is None: + confirm_param = "none" + + commit = not module.check_mode + comment = module.params["comment"] + confirm = None + if confirm_param in ("automatic", "manual"): + confirm = module.params["confirm_timeout"] + + diff = load_config( + module, + ["load %s" % remote_path], + commit=commit, + comment=comment, + confirm=confirm, + ) + if confirm_param == "automatic" and diff and not module.check_mode: + run_commands(module, ["configure", "confirm", "exit"]) + + result["commands"] = ["load %s" % remote_path] + result["filtered"] = [] + result["changed"] = bool(diff) + + if module._diff: + result["diff"] = {"prepared": diff} + + def main(): backup_spec = dict(filename=dict(), dir_path=dict(type="path")) argument_spec = dict( src=dict(type="path"), lines=dict(type="list", elements="str"), - match=dict(default="line", choices=["line", "none"]), + match=dict(default="line", choices=["line", "enforce", "none"]), comment=dict(default=DEFAULT_COMMENT), + confirm=dict(choices=["automatic", "manual", "none"], default=None), + confirm_timeout=dict(type="int", default=10), config=dict(), backup=dict(type="bool", default=False), backup_options=dict(type="dict", options=backup_spec), save=dict(type="bool", default=False), + replace=dict(type="str", default="line", choices=["line", "config"]), + allow_password_change=dict( + default="plaintext", + choices=["all", "encrypted", "plaintext", "none"], + ), ) mutually_exclusive = [("lines", "src")] + required_if = [("replace", "config", ["src"])] module = AnsibleModule( argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True, ) @@ -353,21 +625,27 @@ def main(): if module.params["backup"]: result["__backup__"] = get_config(module=module) - if any((module.params["src"], module.params["lines"])): + if module.params["replace"] == "config": + run_replace_config(module, result) + elif any((module.params["src"], module.params["lines"])): run(module, result) if module.params["save"]: diff = run_commands(module, commands=["configure", "compare saved"])[1] if diff not in { "[edit]", - "No changes between working and saved configurations.\n\n[edit]" + "No changes between working and saved configurations.\n\n[edit]", }: if not module.check_mode: run_commands(module, commands=["save"]) result["changed"] = True run_commands(module, commands=["exit"]) - if result.get("changed") and any((module.params["src"], module.params["lines"])): + if ( + result.get("changed") + and module.params["replace"] != "config" + and any((module.params["src"], module.params["lines"])) + ): msg = ( "To ensure idempotency and correct diff the input configuration lines should be" " similar to how they appear if present in" diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py index a999bd31..3d6d1b05 100644 --- a/plugins/modules/vyos_facts.py +++ b/plugins/modules/vyos_facts.py @@ -5,7 +5,6 @@ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function - __metaclass__ = type """ The module file for vyos_facts @@ -28,7 +27,7 @@ author: extends_documentation_fragment: - vyos.vyos.vyos notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: gather_subset: diff --git a/plugins/modules/vyos_file.py b/plugins/modules/vyos_file.py new file mode 100644 index 00000000..312a7a74 --- /dev/null +++ b/plugins/modules/vyos_file.py @@ -0,0 +1,520 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VyOS maintainers and contributors +# 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 + +DOCUMENTATION = """ +module: vyos_file +short_description: Manage files, directories, and their ownership on VyOS devices +description: + - Creates, updates, or removes a file or directory on a VyOS device, optionally + pushing content from a local file (I(src)) or inline text (I(content)), and + setting owner/group/mode via sudo chown/chmod. + - This module does not touch the configuration tree (config.boot). It manages + arbitrary filesystem paths such as certificates or auth files under + /config/auth/, which are not tracked by commit/save/rollback. + - All logic runs inside this module's main(), using the standard + get_connection()/run_commands() pattern shared with vyos_command — there is + no dedicated action plugin; this module uses the shared generic vyos action + plugin like every other module in the collection. +version_added: "6.0.0" +author: + - VyOS maintainers and contributors (@vyos) +options: + dest: + description: Absolute path to the remote file or directory to manage. + type: path + required: true + state: + description: Whether the path should exist (present) or be removed (absent). + type: str + choices: [present, absent] + default: present + src: + description: + - Path to a local file (on the Ansible controller) whose content should be + pushed to I(dest). Transferred via a real SCP session over the + connection's own persistent socket (the same mechanism + M(ansible.netcommon.net_put) uses), never placed inside a command + string. Mutually exclusive with I(content). + - File bytes are uploaded exactly as they exist on disk — Ansible does + not render Jinja expressions inside the file's contents for I(src), + only in the option values of the task itself (e.g. a templated path + string). To push templated text, render it first with the C(template) + lookup and pass the result via I(content) instead. + type: path + content: + description: + - Inline text content to write to I(dest). Marked no_log, since this module + is commonly used to push credential material. Mutually exclusive with + I(src). + - Since I(content) is a normal string-type module option, Ansible renders + any Jinja expressions in it (e.g. C({{ my_var }})) before this module + ever runs, the same as any other option value — no special templating + support is implemented by this module itself. + type: str + owner: + description: Name of the user that should own I(dest). + type: str + group: + description: Name of the group that should own I(dest). + type: str + mode: + description: + - Permission bits for I(dest), as a string (e.g. '0600'). Compared against + stat output after normalizing to 4 digits; '600' and '0600' are treated + as equivalent. + type: str + become: + description: Whether to prefix remote commands with sudo. + type: bool + default: true +notes: + - This module works with connection C(ansible.netcommon.network_cli). + - File state managed by this module is independent of VyOS's config revision + system. A rollback to a previous config revision will not revert changes + made by this module. + - Paths under I(/config/auth) are deliberately setgid C(vyattacfg) by VyOS's + own config-management convention (see vyos.dev T2713). If I(mode) is given + with a leading digit of C(0) (e.g. C('0750')), this module compares only + the rwx bits and will not report a diff for VyOS's own setgid bit. To + manage the setgid/setuid/sticky bit explicitly, pass a non-zero leading + digit (e.g. C('2750')). +""" + +EXAMPLES = """ +- name: ensure the auth directory exists with correct ownership + vyos.vyos.vyos_file: + dest: /config/auth/office-vpn + owner: openvpn + group: openvpn + mode: '0750' + +- name: push a client certificate with correct ownership + vyos.vyos.vyos_file: + dest: /config/auth/office-vpn/client.pem + src: files/office-vpn-client.pem + owner: openvpn + group: openvpn + mode: '0600' + +- name: remove a stale cert + vyos.vyos.vyos_file: + dest: /config/auth/old-vpn/client.pem + state: absent + +- name: push templated LDAP auth config (content is rendered by Ansible before this module runs) + vyos.vyos.vyos_file: + dest: /config/auth/office-vpn/ldap-auth.config + content: "{{ lookup('template', 'ldap_auth.config.j2') }}" + owner: openvpn + group: openvpn + mode: '0640' +""" + +RETURN = """ +diff_fields: + description: Fields that differed between requested and actual state and were converged. + returned: always + type: list + elements: str + sample: ["owner", "mode", "content"] +""" + +import hashlib +import os +import re +import shlex +import tempfile +import uuid + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( + get_connection, + run_commands, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos_file import ( + build_want, + diff_want_have, + parse_stat, +) + + +ARGUMENT_SPEC = dict( + dest=dict(type="path", required=True), + state=dict(type="str", choices=["present", "absent"], default="present"), + src=dict(type="path"), + content=dict(type="str", no_log=True), + owner=dict(type="str"), + group=dict(type="str"), + mode=dict(type="str"), + become=dict(type="bool", default=True), +) + + +def get_have(module, become, dest, need_content_hash=False): + quoted_dest = shlex.quote(dest) + # check_rc=False is required here: a missing path is a normal, expected + # outcome on first-run creation, not a failure. With the default + # check_rc=True, run_commands() would call module.fail_json() on every + # "file doesn't exist yet" case, which is exactly the case we need to + # handle gracefully to build `have`. + responses = run_commands( + module, + ["{0}stat --format='%a %U %G %s' {1}".format(become, quoted_dest)], + check_rc=False, + ) + out = responses[0] if responses else "" + + if not out: + return None + if "No such file" in out: + return None + + have = parse_stat(out) + if have is None: + # Anything that isn't the specific "doesn't exist" message and + # doesn't parse as valid stat output is a real problem — permission + # denied, I/O error, unexpected format, etc. Fail loudly rather than + # silently treating it as "create it", which could otherwise lead + # this module to attempt mkdir/chown/chmod against a path it + # actually has no real visibility into. + module.fail_json( + msg="vyos_file: unexpected stat output for {0}: {1}".format(dest, out.strip()), + ) + + if need_content_hash: + # Only hash when content comparison actually matters (src/content + # given) — no need to pay this cost for plain directory/ownership + # management. Without this, `have["content_hash"]` would always be + # None, so `content` would show as "different" forever, even right + # after a successful write. + hash_responses = run_commands( + module, + ["{0}sha256sum {1}".format(become, quoted_dest)], + check_rc=False, + ) + hash_out = hash_responses[0] if hash_responses else "" + # sha256sum output format: "<hex digest> <path>" + parts = hash_out.strip().split() + if parts and len(parts[0]) == 64 and all(c in "0123456789abcdef" for c in parts[0].lower()): + have["content_hash"] = parts[0] + # else: leave content_hash unset — a malformed/errored sha256sum + # (e.g. the file vanished in a race between stat and sha256sum) + # should surface as a real diff on the next comparison, not get + # silently recorded as a bogus "hash". + + return have + + +_OCTAL_DIGIT_TO_SYMBOLIC = { + "0": "", + "1": "x", + "2": "w", + "3": "wx", + "4": "r", + "5": "rx", + "6": "rw", + "7": "rwx", +} + + +def _rwx_digits_to_symbolic_mode(mode4): + """Convert the last 3 digits of a normalized 4-digit mode string into a + symbolic chmod argument (e.g. "0750" -> "u=rwx,g=rx,o="). Symbolic mode + assignment for u/g/o only touches those classes — unlike any numeric + chmod form, it leaves existing setuid/setgid/sticky bits untouched + unless explicitly referenced (u+s, g+s, +t), which is exactly the + "special bits are unmanaged for implicit mode requests" guarantee this + module's docs and diff logic already promise but a plain numeric chmod + would silently violate. + """ + u, g, o = mode4[-3], mode4[-2], mode4[-1] + return "u={0},g={1},o={2}".format( + _OCTAL_DIGIT_TO_SYMBOLIC[u], + _OCTAL_DIGIT_TO_SYMBOLIC[g], + _OCTAL_DIGIT_TO_SYMBOLIC[o], + ) + + +def _build_chmod_command(become, mode4, quoted_dest): + if mode4[0] == "0": + # Implicit special bits (caller didn't ask for them): use symbolic + # mode so existing setuid/setgid/sticky bits survive. A numeric + # chmod here — even a bare 3-digit form — always explicitly sets + # the special-bits digit to 0, silently clearing e.g. VyOS's own + # setgid convention on /config/auth (vyos.dev T2713) the moment any + # rwx change is needed, rather than genuinely leaving it unmanaged. + symbolic = _rwx_digits_to_symbolic_mode(mode4) + return "{0}chmod {1} {2}".format(become, shlex.quote(symbolic), quoted_dest) + # Explicit non-zero leading digit: caller wants exact control over + # special bits too, so a plain numeric chmod is correct here. + return "{0}chmod {1} {2}".format(become, shlex.quote(mode4), quoted_dest) + + +def local_content_hash(params): + if params.get("src"): + h = hashlib.sha256() + with open(params["src"], "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + if params.get("content") is not None: + return hashlib.sha256(params["content"].encode()).hexdigest() + return None + + +def read_local_bytes(params): + if params.get("src"): + with open(params["src"], "rb") as f: + return f.read() + if params.get("content") is not None: + return params["content"].encode() + return None + + +def push_content_via_scp(module, connection, become, dest, params): + # Real SCP transfer over the connection's own persistent SSH session — + # content/src bytes never appear inside a command string sent through + # run_commands(). The earlier base64-in-a-shell-command approach was + # only ever encoded, not encrypted, and remained fully readable to + # anything logging connection traffic (e.g. persistent connection + # logging), regardless of no_log on the task — a real problem given + # this module's actual purpose (VPN certs, LDAP credentials). + # + # net_put's own action plugin uses this exact mechanism — connection + # here is get_connection(module), the same Connection(module._socket_path) + # JSON-RPC proxy net_put builds via Connection(socket_path) — so this is + # not action-plugin-only, despite that being true historically for some + # other network_cli file-transfer patterns. + # + # connection.copy_file() writes as the connecting user with NO `become` + # applied — it has no concept of sudo. That's fine for a destination + # the connecting user already has access to (e.g. /config/auth, which + # `vyos` can write via its vyattacfg group membership), but it would + # fail outright against a genuinely protected destination. So: always + # transfer to a /tmp staging path the connecting user can unconditionally + # write to, then relocate it into the real `dest` via a sudo-prefixed + # `mv` — `mv` only ever references paths, never content, so this still + # never puts secret material inside a command string. + cleanup_local = False + if params.get("src"): + local_path = params["src"] + else: + data = read_local_bytes(params) + fd, local_path = tempfile.mkstemp(prefix="vyos_file_") + cleanup_local = True + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + except Exception: + os.remove(local_path) + raise + + remote_staging_path = "/tmp/.vyos_file_staging_{0}".format(uuid.uuid4().hex) + try: + timeout = connection.get_option("persistent_command_timeout") + connection.copy_file( + source=local_path, + destination=remote_staging_path, + proto="scp", + timeout=timeout, + ) + finally: + if cleanup_local: + os.remove(local_path) + + run_commands( + module, + [ + "{0}mv {1} {2}".format( + become, + shlex.quote(remote_staging_path), + shlex.quote(dest), + ), + ], + ) + + +def converge(module, become, dest, want, diff, params): + cmds = [] + quoted_dest = shlex.quote(dest) + + if want["state"] == "absent": + cmds.append("{0}rm -rf {1}".format(become, quoted_dest)) + run_commands(module, cmds) + post_have = get_have(module, become, dest) + if post_have is not None: + module.fail_json( + msg="vyos_file: removal of {0} did not take effect".format(dest), + ) + return + + if "content" in diff: + connection = get_connection(module) + push_content_via_scp(module, connection, become, dest, params) + elif "state" in diff and have_is_missing(diff): + cmds.append("{0}mkdir -p {1}".format(become, quoted_dest)) + + if "owner" in diff and "group" in diff: + cmds.append( + "{0}chown {1}:{2} {3}".format( + become, + shlex.quote(want["owner"]), + shlex.quote(want["group"]), + quoted_dest, + ), + ) + elif "owner" in diff: + cmds.append( + "{0}chown {1} {2}".format(become, shlex.quote(want["owner"]), quoted_dest), + ) + elif "group" in diff: + cmds.append( + "{0}chgrp {1} {2}".format(become, shlex.quote(want["group"]), quoted_dest), + ) + + if "mode" in diff: + cmds.append(_build_chmod_command(become, want["mode"], quoted_dest)) + + if cmds: + run_commands(module, cmds) + + # run_commands() only confirms the CLI accepted each command line + # syntactically — it does NOT confirm the underlying binary succeeded. + # A chown against a nonexistent group, for example, prints an error to + # stdout but the CLI wrapper still reports the line as "executed"; we + # would otherwise report changed=true for a write that silently did + # nothing. Re-stat and compare against `want` to catch this class of + # failure before returning success. + post_have = get_have( + module, + become, + dest, + need_content_hash=want.get("content_hash") is not None, + ) + post_diff = diff_want_have(want, post_have) + if post_diff: + module.fail_json( + msg=( + "vyos_file converged but post-check found remaining " + "differences — one or more commands likely failed silently " + "at the OS level (e.g. chown to a nonexistent user/group): " + "{0}".format(post_diff) + ), + ) + + +def have_is_missing(diff): + return diff.get("state") == (None, "present") + + +def validate_dest(module, dest): + # dest is type=path in ARGUMENT_SPEC, which expands ~ and env vars but + # does NOT enforce absoluteness — a relative value would resolve against + # whatever the underlying shell's cwd happens to be, an unintended and + # unpredictable target. And since this module issues raw `rm -rf`, + # `chmod`, `chown` against dest with no config-tree safety net, a + # dest of "/" (or anything that normalizes to it) combined with + # state=absent would attempt to recursively remove the entire + # filesystem. Both must be rejected before any stat/converge runs. + if not os.path.isabs(dest): + module.fail_json( + msg="vyos_file: dest must be an absolute path, got {0!r}".format(dest), + ) + normalized = os.path.normpath(dest) + # normalized == "/" alone is insufficient: os.path.normpath preserves + # "//" as-is (a POSIX quirk permitting implementation-defined behavior + # for exactly two leading slashes), so dest="//" would otherwise bypass + # this check entirely. Stripping all slashes catches "/", "//", "///", + # etc. uniformly. + if normalized.strip("/") == "": + module.fail_json( + msg=( + "vyos_file: refusing to manage the root filesystem path " + "(dest normalized to {0!r}): {1!r}".format(normalized, dest) + ), + ) + + +_MODE_RE = re.compile(r"^[0-7]{3,4}$") + + +def validate_mode(module, mode): + # _normalize_mode() (module_utils) does str(mode).zfill(4)[-4:], which + # for genuinely invalid input silently mangles it into something that + # LOOKS valid rather than rejecting it — e.g. "10640" (5 digits, an + # obvious typo for a 4-digit mode) becomes "0640" by truncation, and + # the module would silently apply permissions the caller never actually + # asked for. Validate strictly here, before that normalization ever + # runs, so malformed input fails loudly instead of being reinterpreted. + if mode is None: + return + if not _MODE_RE.match(mode): + module.fail_json( + msg=( + "vyos_file: mode must be an octal string of 3 or 4 digits " + "(0-7 only), got {0!r}".format(mode) + ), + ) + + +def validate_src(module, src): + # local_content_hash()/read_local_bytes() do plain open(src, "rb") + # calls with no existence/type/permission check. A missing file, a + # directory passed where a file is expected, or an unreadable path + # would otherwise surface as an unhandled Python traceback instead of + # a clean module error — and this happens even under check_mode, since + # content-hashing runs before the check-mode short-circuit. + if src is None: + return + if not os.path.exists(src): + module.fail_json(msg="vyos_file: src not found: {0!r}".format(src)) + if os.path.isdir(src): + module.fail_json( + msg="vyos_file: src is a directory, expected a file: {0!r}".format(src), + ) + if not os.access(src, os.R_OK): + module.fail_json(msg="vyos_file: src is not readable: {0!r}".format(src)) + + +def main(): + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + mutually_exclusive=[["src", "content"]], + supports_check_mode=True, + ) + + dest = module.params["dest"] + validate_dest(module, dest) + validate_mode(module, module.params.get("mode")) + validate_src(module, module.params.get("src")) + + become = "sudo " if module.params.get("become", True) else "" + + want = build_want(module.params, local_content_hash(module.params)) + have = get_have( + module, + become, + dest, + need_content_hash=want.get("content_hash") is not None, + ) + diff = diff_want_have(want, have) + + result = {"changed": bool(diff), "diff_fields": list(diff.keys())} + + if module.check_mode or not diff: + module.exit_json(**result) + + converge(module, become, dest, want, diff, module.params) + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py index e952ae50..4967a564 100644 --- a/plugins/modules/vyos_firewall_global.py +++ b/plugins/modules/vyos_firewall_global.py @@ -28,7 +28,6 @@ The module file for vyos_firewall_global from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -46,7 +45,8 @@ description: VyOS devices. version_added: '1.0.0' notes: -- Tested against VyOS 1.3.8. +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. +- The provided examples of commands are valid for VyOS 1.4+ - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: @@ -280,6 +280,91 @@ options: - notice - info - debug + zone: + description: + - Defines a firewall zone. + type: list + elements: dict + suboptions: + name: + description: + - Name of the firewall zone. + type: str + required: true + description: + description: + - Allows you to specify a brief description for the firewall zone. + type: str + default_log: + description: + - Specifies whether or not to log packets for the firewall zone. + type: bool + local_zone: + description: + - Specifies whether or not the zone is local. + type: bool + default_action: + description: + - Specifies the default action for the zone. + type: str + default: drop + choices: + - drop + - reject + interfaces: + description: + - Specifies the interfaces associated with the zone. + type: list + elements: str + intra_zone_filtering: + description: + - Specifies a policy for intra-zone filtering. + type: dict + suboptions: + action: + description: + - Action for intra-zone traffic. + type: str + choices: + - accept + - drop + firewall: + description: + - Firewall ruleset to apply to intra-zone traffic. + type: dict + suboptions: + name: + description: + - Name of the firewall ruleset to apply to intra-zone traffic. + type: str + ipv6_name: + description: + - Name of the IPv6 firewall ruleset to apply to intra-zone traffic. + type: str + sources: + description: + - Specifies the source zones for the firewall rules. + type: list + elements: dict + suboptions: + zone: + description: + - Name of the source zone. + type: str + required: true + firewall: + description: + - Firewall ruleset to apply to the source zone. + type: dict + suboptions: + name: + description: + - Name of the firewall ruleset to apply to the source zone. + type: str + ipv6_name: + description: + - Name of the IPv6 firewall ruleset to apply to the source zone. + type: str running_config: description: - > @@ -373,7 +458,7 @@ EXAMPLES = """ # "set firewall global-options send-redirects 'enable'", # "set firewall global-options config-trap 'enable'", # "set firewall global-options state-policy established action 'accept'", -# "set firewall global-options state-policy established log 'enable'", +# "set firewall global-options state-policy established log, # "set firewall global-options state-policy established log-level 'emerg'", # "set firewall global-options state-policy invalid action 'reject'", # "set firewall global-options broadcast-ping 'enable'", @@ -1189,7 +1274,7 @@ EXAMPLES = """ # "set firewall global-options send-redirects 'enable'", # "set firewall global-options config-trap 'enable'", # "set firewall global-options state-policy established action 'accept'", -# "set firewall global-options state-policy established log 'enable'", +# "set firewall global-options state-policy established log, # "set firewall global-options state-policy invalid action 'reject'", # "set firewall global-options broadcast-ping 'enable'", # "set firewall global-options all-ping 'enable'", diff --git a/plugins/modules/vyos_firewall_interfaces.py b/plugins/modules/vyos_firewall_interfaces.py index ae17bc13..a3210b65 100644 --- a/plugins/modules/vyos_firewall_interfaces.py +++ b/plugins/modules/vyos_firewall_interfaces.py @@ -28,7 +28,6 @@ The module file for vyos_firewall_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { diff --git a/plugins/modules/vyos_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py index 850299ff..96cf271b 100644 --- a/plugins/modules/vyos_firewall_rules.py +++ b/plugins/modules/vyos_firewall_rules.py @@ -28,7 +28,6 @@ The module file for vyos_firewall_rules from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -47,7 +46,8 @@ author: - Rohit Thakur (@rohitthakur2590) - Gaige B. Paulsen (@gaige) notes: -- Tested against VyOS 1.3.8. +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. +- The provided examples of commands are valid for VyOS 1.4+ - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). @@ -89,8 +89,10 @@ options: - reject (Drop and notify source if no prior rules are hit) - accept (Accept if no prior rules are hit) - jump (Jump to another rule-set, 1.4+) + - return (Return from the current chain and continue at the next rule of the last chain, 1.4+) + - continue (Continue parsing next rule, 1.4+) type: str - choices: ['drop', 'reject', 'accept', 'jump'] + choices: ['drop', 'reject', 'accept', 'jump', 'return', 'continue'] default_jump_target: description: - Default jump target if the default action is jump. @@ -134,6 +136,7 @@ options: - continue - return - jump + - offload - queue - synproxy destination: @@ -308,6 +311,10 @@ options: - Option to log packets matching rule. type: str choices: ['disable', 'enable'] + offload_target: + description: + - Match flowtable object. + type: str outbound_interface: description: - Match outbound interface. @@ -570,14 +577,14 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall name Downlink default-action 'accept' -# set firewall name Downlink description 'IPv4 INBOUND rule set' -# set firewall name Downlink rule 501 action 'accept' -# set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible' -# set firewall name Downlink rule 501 ipsec 'match-ipsec' -# set firewall name Downlink rule 502 action 'reject' -# set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible' -# set firewall name Downlink rule 502 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink default-action 'accept' +# set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set' +# set firewall ipv4 name Downlink rule 501 action 'accept' +# set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible' +# set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink rule 502 action 'reject' +# set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible' +# set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec' - name: Delete attributes of given firewall rules. vyos.vyos.vyos_firewall_rules: @@ -619,7 +626,7 @@ EXAMPLES = """ # } # ] # "commands": [ -# "delete firewall name Downlink" +# "delete firewall ipv4 name Downlink" # ] # # "after": [] @@ -635,25 +642,25 @@ EXAMPLES = """ # ------------- # # vyos@vyos:~$ show configuration commands| grep firewall -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' # set firewall group address-group 'inbound' -# set firewall name Downlink default-action 'accept' -# set firewall name Downlink description 'IPv4 INBOUND rule set' -# set firewall name Downlink rule 501 action 'accept' -# set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible' -# set firewall name Downlink rule 501 ipsec 'match-ipsec' -# set firewall name Downlink rule 502 action 'reject' -# set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible' -# set firewall name Downlink rule 502 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink default-action 'accept' +# set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set' +# set firewall ipv4 name Downlink rule 501 action 'accept' +# set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible' +# set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink rule 502 action 'reject' +# set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible' +# set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec' - name: Delete attributes of given firewall rules. vyos.vyos.vyos_firewall_rules: @@ -717,23 +724,23 @@ EXAMPLES = """ # } # ] # "commands": [ -# "delete firewall name" +# "delete firewall ipv4 name" # ] # # "after": [] # After state # ------------ # vyos@vyos:~$ show configuration commands| grep firewall -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' # Using deleted to delete all the the firewall rules when provided config is empty @@ -743,14 +750,14 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall name Downlink default-action 'accept' -# set firewall name Downlink description 'IPv4 INBOUND rule set' -# set firewall name Downlink rule 501 action 'accept' -# set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible' -# set firewall name Downlink rule 501 ipsec 'match-ipsec' -# set firewall name Downlink rule 502 action 'reject' -# set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible' -# set firewall name Downlink rule 502 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink default-action 'accept' +# set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set' +# set firewall ipv4 name Downlink rule 501 action 'accept' +# set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible' +# set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink rule 502 action 'reject' +# set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible' +# set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec' # - name: Delete attributes of given firewall rules. vyos.vyos.vyos_firewall_rules: @@ -788,7 +795,7 @@ EXAMPLES = """ # } # ] # "commands": [ -# "delete firewall name" +# "delete firewall ipv4 name" # ] # # "after": [] @@ -860,35 +867,33 @@ EXAMPLES = """ # before": [] # # "commands": [ -# "set firewall ipv6-name UPLINK default-action 'accept'", -# "set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set'", -# "set firewall ipv6-name UPLINK rule 1 action 'accept'", -# "set firewall ipv6-name UPLINK rule 1", -# "set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible'", -# "set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec'", -# "set firewall ipv6-name UPLINK rule 2 action 'accept'", -# "set firewall ipv6-name UPLINK rule 2", -# "set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible'", -# "set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec'", -# "set firewall name INBOUND default-action 'accept'", -# "set firewall name INBOUND description 'IPv4 INBOUND rule set'", -# "set firewall name INBOUND rule 101 action 'accept'", -# "set firewall name INBOUND rule 101", -# "set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", -# "set firewall name INBOUND rule 101 ipsec 'match-ipsec'", -# "set firewall name INBOUND rule 102 action 'reject'", -# "set firewall name INBOUND rule 102", -# "set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", -# "set firewall name INBOUND rule 102 ipsec 'match-ipsec'", -# "set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible'", -# "set firewall name INBOUND rule 103 destination group address-group inbound", -# "set firewall name INBOUND rule 103", -# "set firewall name INBOUND rule 103 source address 192.0.2.0", -# "set firewall name INBOUND rule 103 state established enable", -# "set firewall name INBOUND rule 103 state related enable", -# "set firewall name INBOUND rule 103 state invalid disable", -# "set firewall name INBOUND rule 103 state new disable", -# "set firewall name INBOUND rule 103 action 'accept'" +# "set firewall ipv6 name UPLINK default-action 'accept'", +# "set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set'", +# "set firewall ipv6 name UPLINK rule 1 action 'accept'", +# "set firewall ipv6 name UPLINK rule 1", +# "set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible'", +# "set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec'", +# "set firewall ipv6 name UPLINK rule 2 action 'accept'", +# "set firewall ipv6 name UPLINK rule 2", +# "set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible'", +# "set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec'", +# "set firewall ipv4 name INBOUND default-action 'accept'", +# "set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set'", +# "set firewall ipv4 name INBOUND rule 101 action 'accept'", +# "set firewall ipv4 name INBOUND rule 101", +# "set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec'", +# "set firewall ipv4 name INBOUND rule 102 action 'reject'", +# "set firewall ipv4 name INBOUND rule 102", +# "set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec'", +# "set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 103 destination group address-group inbound", +# "set firewall ipv4 name INBOUND rule 103", +# "set firewall ipv4 name INBOUND rule 103 source address 192.0.2.0", +# "set firewall ipv4 name INBOUND rule 103 state established", +# "set firewall ipv4 name INBOUND rule 103 state related", +# "set firewall ipv4 name INBOUND rule 103 action 'accept'" # ] # # "after": [ @@ -966,30 +971,28 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 102 action 'reject' -# set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible' -# set firewall name INBOUND rule 102 ipsec 'match-ipsec' -# set firewall name INBOUND rule 103 action 'accept' -# set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible' -# set firewall name INBOUND rule 103 destination group address-group 'inbound' -# set firewall name INBOUND rule 103 source address '192.0.2.0' -# set firewall name INBOUND rule 103 state established 'enable' -# set firewall name INBOUND rule 103 state invalid 'disable' -# set firewall name INBOUND rule 103 state new 'disable' -# set firewall name INBOUND rule 103 state related 'enable' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 102 action 'reject' +# set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 103 action 'accept' +# set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 103 destination group address-group 'inbound' +# set firewall ipv4 name INBOUND rule 103 source address '192.0.2.0' +# set firewall ipv4 name INBOUND rule 103 state established +# set firewall ipv4 name INBOUND rule 103 state related # Using replaced @@ -999,30 +1002,28 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 102 action 'reject' -# set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible' -# set firewall name INBOUND rule 102 ipsec 'match-ipsec' -# set firewall name INBOUND rule 103 action 'accept' -# set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible' -# set firewall name INBOUND rule 103 destination group address-group 'inbound' -# set firewall name INBOUND rule 103 source address '192.0.2.0' -# set firewall name INBOUND rule 103 state established 'enable' -# set firewall name INBOUND rule 103 state invalid 'disable' -# set firewall name INBOUND rule 103 state new 'disable' -# set firewall name INBOUND rule 103 state related 'enable' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 102 action 'reject' +# set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 103 action 'accept' +# set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 103 destination group address-group 'inbound' +# set firewall ipv4 name INBOUND rule 103 source address '192.0.2.0' +# set firewall ipv4 name INBOUND rule 103 state established +# set firewall ipv4 name INBOUND rule 103 state related # - name: >- Replace device configurations of listed firewall rules with provided @@ -1126,14 +1127,14 @@ EXAMPLES = """ # ] # # "commands": [ -# "delete firewall ipv6-name UPLINK rule 1", -# "delete firewall ipv6-name UPLINK rule 2", -# "delete firewall name INBOUND rule 102", -# "delete firewall name INBOUND rule 103", -# "set firewall name INBOUND rule 104 action 'reject'", -# "set firewall name INBOUND rule 104 description 'Rule 104 is configured by Ansible'", -# "set firewall name INBOUND rule 104", -# "set firewall name INBOUND rule 104 ipsec 'match-none'" +# "delete firewall ipv6 name UPLINK rule 1", +# "delete firewall ipv6 name UPLINK rule 2", +# "delete firewall ipv4 name INBOUND rule 102", +# "delete firewall ipv4 name INBOUND rule 103", +# "set firewall ipv4 name INBOUND rule 104 action 'reject'", +# "set firewall ipv4 name INBOUND rule 104 description 'Rule 104 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 104", +# "set firewall ipv4 name INBOUND rule 104 ipsec 'match-none'" # ] # # "after": [ @@ -1178,16 +1179,16 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 104 action 'reject' -# set firewall name INBOUND rule 104 description 'Rule 104 is configured by Ansible' -# set firewall name INBOUND rule 104 ipsec 'match-none' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 104 action 'reject' +# set firewall ipv4 name INBOUND rule 104 description 'Rule 104 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 104 ipsec 'match-none' # Using overridden @@ -1197,16 +1198,16 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 104 action 'reject' -# set firewall name INBOUND rule 104 description 'Rule 104 is configured by Ansible' -# set firewall name INBOUND rule 104 ipsec 'match-none' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 104 action 'reject' +# set firewall ipv4 name INBOUND rule 104 description 'Rule 104 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 104 ipsec 'match-none' # - name: Overrides all device configuration with provided configuration vyos.vyos.vyos_firewall_rules: @@ -1270,18 +1271,18 @@ EXAMPLES = """ # ] # # "commands": [ -# "delete firewall ipv6-name UPLINK", -# "delete firewall name INBOUND", -# "set firewall name Downlink default-action 'accept'", -# "set firewall name Downlink description 'IPv4 INBOUND rule set'", -# "set firewall name Downlink rule 501 action 'accept'", -# "set firewall name Downlink rule 501", -# "set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible'", -# "set firewall name Downlink rule 501 ipsec 'match-ipsec'", -# "set firewall name Downlink rule 502 action 'reject'", -# "set firewall name Downlink rule 502", -# "set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible'", -# "set firewall name Downlink rule 502 ipsec 'match-ipsec'" +# "delete firewall ipv6 name UPLINK", +# "delete firewall ipv4 name INBOUND", +# "set firewall ipv4 name Downlink default-action 'accept'", +# "set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set'", +# "set firewall ipv4 name Downlink rule 501 action 'accept'", +# "set firewall ipv4 name Downlink rule 501", +# "set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible'", +# "set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec'", +# "set firewall ipv4 name Downlink rule 502 action 'reject'", +# "set firewall ipv4 name Downlink rule 502", +# "set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible'", +# "set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec'" # # # "after": [ @@ -1317,14 +1318,14 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall name Downlink default-action 'accept' -# set firewall name Downlink description 'IPv4 INBOUND rule set' -# set firewall name Downlink rule 501 action 'accept' -# set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible' -# set firewall name Downlink rule 501 ipsec 'match-ipsec' -# set firewall name Downlink rule 502 action 'reject' -# set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible' -# set firewall name Downlink rule 502 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink default-action 'accept' +# set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set' +# set firewall ipv4 name Downlink rule 501 action 'accept' +# set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible' +# set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec' +# set firewall ipv4 name Downlink rule 502 action 'reject' +# set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible' +# set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec' # Using gathered @@ -1334,30 +1335,28 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 102 action 'reject' -# set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible' -# set firewall name INBOUND rule 102 ipsec 'match-ipsec' -# set firewall name INBOUND rule 103 action 'accept' -# set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible' -# set firewall name INBOUND rule 103 destination group address-group 'inbound' -# set firewall name INBOUND rule 103 source address '192.0.2.0' -# set firewall name INBOUND rule 103 state established 'enable' -# set firewall name INBOUND rule 103 state invalid 'disable' -# set firewall name INBOUND rule 103 state new 'disable' -# set firewall name INBOUND rule 103 state related 'enable' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 102 action 'reject' +# set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 103 action 'accept' +# set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 103 destination group address-group 'inbound' +# set firewall ipv4 name INBOUND rule 103 source address '192.0.2.0' +# set firewall ipv4 name INBOUND rule 103 state established +# set firewall ipv4 name INBOUND rule 103 state related # - name: Gather listed firewall rules with provided configurations vyos.vyos.vyos_firewall_rules: @@ -1445,30 +1444,28 @@ EXAMPLES = """ # # vyos@vyos:~$ show configuration commands| grep firewall # set firewall group address-group 'inbound' -# set firewall ipv6-name UPLINK default-action 'accept' -# set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set' -# set firewall ipv6-name UPLINK rule 1 action 'accept' -# set firewall ipv6-name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 1 ipsec 'match-ipsec' -# set firewall ipv6-name UPLINK rule 2 action 'accept' -# set firewall ipv6-name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' -# set firewall ipv6-name UPLINK rule 2 ipsec 'match-ipsec' -# set firewall name INBOUND default-action 'accept' -# set firewall name INBOUND description 'IPv4 INBOUND rule set' -# set firewall name INBOUND rule 101 action 'accept' -# set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible' -# set firewall name INBOUND rule 101 ipsec 'match-ipsec' -# set firewall name INBOUND rule 102 action 'reject' -# set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible' -# set firewall name INBOUND rule 102 ipsec 'match-ipsec' -# set firewall name INBOUND rule 103 action 'accept' -# set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible' -# set firewall name INBOUND rule 103 destination group address-group 'inbound' -# set firewall name INBOUND rule 103 source address '192.0.2.0' -# set firewall name INBOUND rule 103 state established 'enable' -# set firewall name INBOUND rule 103 state invalid 'disable' -# set firewall name INBOUND rule 103 state new 'disable' -# set firewall name INBOUND rule 103 state related 'enable' +# set firewall ipv6 name UPLINK default-action 'accept' +# set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set' +# set firewall ipv6 name UPLINK rule 1 action 'accept' +# set firewall ipv6 name UPLINK rule 1 description 'Fwipv6-Rule 1 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 1 ipsec 'match-ipsec' +# set firewall ipv6 name UPLINK rule 2 action 'accept' +# set firewall ipv6 name UPLINK rule 2 description 'Fwipv6-Rule 2 is configured by Ansible' +# set firewall ipv6 name UPLINK rule 2 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND default-action 'accept' +# set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set' +# set firewall ipv4 name INBOUND rule 101 action 'accept' +# set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 102 action 'reject' +# set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec' +# set firewall ipv4 name INBOUND rule 103 action 'accept' +# set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible' +# set firewall ipv4 name INBOUND rule 103 destination group address-group 'inbound' +# set firewall ipv4 name INBOUND rule 103 source address '192.0.2.0' +# set firewall ipv4 name INBOUND rule 103 state established +# set firewall ipv4 name INBOUND rule 103 state related # Using rendered @@ -1518,27 +1515,25 @@ EXAMPLES = """ # # # "rendered": [ -# "set firewall ipv6-name UPLINK default-action 'accept'", -# "set firewall ipv6-name UPLINK description 'This is ipv6 specific rule-set'", -# "set firewall name INBOUND default-action 'accept'", -# "set firewall name INBOUND description 'IPv4 INBOUND rule set'", -# "set firewall name INBOUND rule 101 action 'accept'", -# "set firewall name INBOUND rule 101", -# "set firewall name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", -# "set firewall name INBOUND rule 101 ipsec 'match-ipsec'", -# "set firewall name INBOUND rule 102 action 'reject'", -# "set firewall name INBOUND rule 102", -# "set firewall name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", -# "set firewall name INBOUND rule 102 ipsec 'match-ipsec'", -# "set firewall name INBOUND rule 103 description 'Rule 103 is configured by Ansible'", -# "set firewall name INBOUND rule 103 destination group address-group inbound", -# "set firewall name INBOUND rule 103", -# "set firewall name INBOUND rule 103 source address 192.0.2.0", -# "set firewall name INBOUND rule 103 state established enable", -# "set firewall name INBOUND rule 103 state related enable", -# "set firewall name INBOUND rule 103 state invalid disable", -# "set firewall name INBOUND rule 103 state new disable", -# "set firewall name INBOUND rule 103 action 'accept'" +# "set firewall ipv6 name UPLINK default-action 'accept'", +# "set firewall ipv6 name UPLINK description 'This is ipv6 specific rule-set'", +# "set firewall ipv4 name INBOUND default-action 'accept'", +# "set firewall ipv4 name INBOUND description 'IPv4 INBOUND rule set'", +# "set firewall ipv4 name INBOUND rule 101 action 'accept'", +# "set firewall ipv4 name INBOUND rule 101", +# "set firewall ipv4 name INBOUND rule 101 description 'Rule 101 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 101 ipsec 'match-ipsec'", +# "set firewall ipv4 name INBOUND rule 102 action 'reject'", +# "set firewall ipv4 name INBOUND rule 102", +# "set firewall ipv4 name INBOUND rule 102 description 'Rule 102 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 102 ipsec 'match-ipsec'", +# "set firewall ipv4 name INBOUND rule 103 description 'Rule 103 is configured by Ansible'", +# "set firewall ipv4 name INBOUND rule 103 destination group address-group inbound", +# "set firewall ipv4 name INBOUND rule 103", +# "set firewall ipv4 name INBOUND rule 103 source address 192.0.2.0", +# "set firewall ipv4 name INBOUND rule 103 state established", +# "set firewall ipv4 name INBOUND rule 103 state related", +# "set firewall ipv4 name INBOUND rule 103 action 'accept'" # ] @@ -1549,14 +1544,14 @@ EXAMPLES = """ vyos.vyos.vyos_firewall_rules: running_config: "set firewall group address-group 'inbound' - set firewall name Downlink default-action 'accept' - set firewall name Downlink description 'IPv4 INBOUND rule set' - set firewall name Downlink rule 501 action 'accept' - set firewall name Downlink rule 501 description 'Rule 501 is configured by Ansible' - set firewall name Downlink rule 501 ipsec 'match-ipsec' - set firewall name Downlink rule 502 action 'reject' - set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible' - set firewall name Downlink rule 502 ipsec 'match-ipsec'" + set firewall ipv4 name Downlink default-action 'accept' + set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set' + set firewall ipv4 name Downlink rule 501 action 'accept' + set firewall ipv4 name Downlink rule 501 description 'Rule 501 is configured by Ansible' + set firewall ipv4 name Downlink rule 501 ipsec 'match-ipsec' + set firewall ipv4 name Downlink rule 502 action 'reject' + set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible' + set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec'" state: parsed # # @@ -1612,21 +1607,21 @@ commands: returned: always type: list sample: - - "set firewall name Downlink default-action 'accept'" - - "set firewall name Downlink description 'IPv4 INBOUND rule set'" - - "set firewall name Downlink rule 501 action 'accept'" - - "set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible'" - - "set firewall name Downlink rule 502 ipsec 'match-ipsec'" + - "set firewall ipv4 name Downlink default-action 'accept'" + - "set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set'" + - "set firewall ipv4 name Downlink rule 501 action 'accept'" + - "set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible'" + - "set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec'" 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 firewall name Downlink default-action 'accept'" - - "set firewall name Downlink description 'IPv4 INBOUND rule set'" - - "set firewall name Downlink rule 501 action 'accept'" - - "set firewall name Downlink rule 502 description 'Rule 502 is configured by Ansible'" - - "set firewall name Downlink rule 502 ipsec 'match-ipsec'" + - "set firewall ipv4 name Downlink default-action 'accept'" + - "set firewall ipv4 name Downlink description 'IPv4 INBOUND rule set'" + - "set firewall ipv4 name Downlink rule 501 action 'accept'" + - "set firewall ipv4 name Downlink rule 502 description 'Rule 502 is configured by Ansible'" + - "set firewall ipv4 name Downlink rule 502 ipsec 'match-ipsec'" gathered: description: Facts about the network resource gathered from the remote device as structured data. returned: when I(state) is C(gathered) 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() diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py index 480b011f..27f2081f 100644 --- a/plugins/modules/vyos_hostname.py +++ b/plugins/modules/vyos_hostname.py @@ -10,7 +10,6 @@ The module file for vyos_hostname from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py index 6125b4b9..98f3aa5a 100644 --- a/plugins/modules/vyos_interfaces.py +++ b/plugins/modules/vyos_interfaces.py @@ -28,7 +28,6 @@ The module file for vyos_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -47,7 +46,7 @@ description: - This module supports managing base attributes of Ethernet, Bonding, VXLAN, Loopback and Virtual Tunnel Interfaces. notes: -- Tested against VyOS 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: @@ -129,6 +128,10 @@ options: - MTU for the virtual sub-interface. - Refer to vendor documentation for valid values. type: int + vrf: + description: + - VRF associated with the interface. + type: str running_config: description: - This option is used only with state I(parsed). diff --git a/plugins/modules/vyos_l3_interfaces.py b/plugins/modules/vyos_l3_interfaces.py index 0d2a5dae..1d3dd20c 100644 --- a/plugins/modules/vyos_l3_interfaces.py +++ b/plugins/modules/vyos_l3_interfaces.py @@ -28,7 +28,6 @@ The module file for vyos_l3_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { diff --git a/plugins/modules/vyos_lag_interfaces.py b/plugins/modules/vyos_lag_interfaces.py index 090021ad..27dc6d0a 100644 --- a/plugins/modules/vyos_lag_interfaces.py +++ b/plugins/modules/vyos_lag_interfaces.py @@ -28,7 +28,6 @@ The module file for vyos_lag_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -46,7 +45,7 @@ description: This module manages attributes of link aggregation groups on VyOS n author: - Rohit Thakur (@rohitthakur2590) notes: -- Tested against VyOS 1.3.8. +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: config: diff --git a/plugins/modules/vyos_lldp_global.py b/plugins/modules/vyos_lldp_global.py index 190f4513..a1c01e23 100644 --- a/plugins/modules/vyos_lldp_global.py +++ b/plugins/modules/vyos_lldp_global.py @@ -28,7 +28,6 @@ The module file for vyos_lldp_global from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -45,7 +44,7 @@ short_description: LLDP global resource module description: This module manages link layer discovery protocol (LLDP) attributes on VyOS devices. notes: -- Tested against VyOS 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: - Rohit Thakur (@rohitthakur2590) diff --git a/plugins/modules/vyos_lldp_interfaces.py b/plugins/modules/vyos_lldp_interfaces.py index 0a8f892b..48cb171f 100644 --- a/plugins/modules/vyos_lldp_interfaces.py +++ b/plugins/modules/vyos_lldp_interfaces.py @@ -28,7 +28,6 @@ The module file for vyos_lldp_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -44,7 +43,7 @@ version_added: '1.0.0' short_description: LLDP interfaces resource module description: This module manages attributes of lldp interfaces on VyOS network devices. notes: -- Tested against VyOS 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: diff --git a/plugins/modules/vyos_logging_global.py b/plugins/modules/vyos_logging_global.py index 9479e7b2..c9443f6a 100644 --- a/plugins/modules/vyos_logging_global.py +++ b/plugins/modules/vyos_logging_global.py @@ -10,7 +10,6 @@ The module file for vyos_logging_global from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -20,10 +19,20 @@ short_description: Logging resource module description: This module manages the logging attributes of Vyos network devices author: Sagar Paul (@KB-perByte) notes: - - Tested against vyos 1.3.8+ + - Tested against VyOS 1.3.8, 1.4.2, 1.5.0, and the rolling release of spring 2025. - This module works with connection C(network_cli). - - The Configuration defaults of the Vyos network devices - are supposed to hinder idempotent behavior of plays + - The Configuration defaults of the VyOS network devices + are supposed to hinder idempotent behavior of plays. + - > + B(VyOS 1.5+ breaking changes): The C(files), C(users), and + C(global_params.archive) options are not supported on VyOS 1.5+. + If provided, they will be ignored with a warning. + The C(global_params) facilities now map to C(set system syslog local) + (was C(set system syslog global)). + Remote hosts now map to C(set system syslog remote) + (was C(set system syslog host)). + The C(marker_interval) and C(preserve_fqdn) options moved to top-level + (was under C(global_params) in the CLI, argspec key unchanged). options: config: description: A list containing dictionary of logging options @@ -85,7 +94,9 @@ options: - debug - all files: - description: logging to file + description: > + Logging to file. B(Not supported on VyOS 1.5+.) If provided on a + 1.5+ device, this option will be ignored with a warning. type: list elements: dict suboptions: @@ -111,7 +122,11 @@ options: facility: *facility severity: *severity global_params: - description: logging to serial console + description: > + Global logging parameters. On VyOS 1.5+, facilities map to + C(set system syslog local), and C(marker_interval)/C(preserve_fqdn) + move to top-level CLI paths. The C(archive) suboption is + B(not supported on VyOS 1.5+) and will be ignored with a warning. type: dict suboptions: state: *state_config @@ -124,7 +139,9 @@ options: description: uses FQDN for logging type: bool hosts: - description: logging to serial console + description: > + Logging to remote hosts. On VyOS 1.5+, maps to + C(set system syslog remote) (was C(set system syslog host)). type: list elements: dict suboptions: @@ -159,7 +176,9 @@ options: suboptions: state: *state_config users: - description: logging to file + description: > + Logging to a local user terminal. B(Not supported on VyOS 1.5+.) + If provided on a 1.5+ device, this option will be ignored with a warning. type: list elements: dict suboptions: @@ -699,6 +718,141 @@ EXAMPLES = """ # ] # } # } +# Using state: merged (VyOS 1.5+) + +# Before state: +# ------------- + +# vyos:~$ show configuration commands | grep syslog + +- name: Apply the provided configuration (VyOS 1.5+) + vyos.vyos.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: err + hosts: + - hostname: 172.16.0.1 + facilities: + - facility: local7 + severity: all + port: 514 + protocol: udp + global_params: + facilities: + - facility: cron + severity: debug + marker_interval: 111 + preserve_fqdn: true + state: merged + +# Commands Fired: +# --------------- + +# "commands": [ +# "set system syslog console facility local7 level err", +# "set system syslog remote 172.16.0.1 facility local7 level all", +# "set system syslog remote 172.16.0.1 port 514", +# "set system syslog remote 172.16.0.1 protocol udp", +# "set system syslog local facility cron level debug", +# "set system syslog marker interval 111", +# "set system syslog preserve-fqdn" +# ], + +# After state: +# ------------ + +# vyos:~$ show configuration commands | grep syslog +# set system syslog console facility local7 level 'err' +# set system syslog local facility cron level 'debug' +# set system syslog marker interval '111' +# set system syslog preserve-fqdn +# set system syslog remote 172.16.0.1 facility local7 level 'all' +# set system syslog remote 172.16.0.1 port '514' +# set system syslog remote 172.16.0.1 protocol 'udp' + +# Using state: gathered (VyOS 1.5+) + +- name: Gather logging config (VyOS 1.5+) + vyos.vyos.vyos_logging_global: + state: gathered + +# Module Execution Result: +# ------------------------ + +# "gathered": { +# "console": { +# "facilities": [ +# { +# "facility": "local7", +# "severity": "err" +# } +# ] +# }, +# "global_params": { +# "facilities": [ +# { +# "facility": "cron", +# "severity": "debug" +# } +# ], +# "marker_interval": 111, +# "preserve_fqdn": true +# }, +# "hosts": [ +# { +# "facilities": [ +# { +# "facility": "local7", +# "severity": "all" +# } +# ], +# "hostname": "172.16.0.1", +# "port": 514, +# "protocol": "udp" +# } +# ] +# }, + +# Using state: rendered (VyOS 1.5+) +# Note: files, users, and global_params.archive are not supported on VyOS 1.5+ +# and will be ignored with a warning if provided. + +- name: Render the provided configuration (VyOS 1.5+) + vyos.vyos.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: err + hosts: + - hostname: 172.16.0.1 + facilities: + - facility: local7 + severity: all + port: 514 + protocol: udp + global_params: + facilities: + - facility: cron + severity: debug + marker_interval: 111 + preserve_fqdn: true + state: rendered + +# Module Execution Result: +# ------------------------ + +# "rendered": [ +# "set system syslog console facility local7 level err", +# "set system syslog remote 172.16.0.1 facility local7 level all", +# "set system syslog remote 172.16.0.1 port 514", +# "set system syslog remote 172.16.0.1 protocol udp", +# "set system syslog local facility cron level debug", +# "set system syslog marker interval 111", +# "set system syslog preserve-fqdn" +# ] """ RETURN = """ diff --git a/plugins/modules/vyos_nat.py b/plugins/modules/vyos_nat.py new file mode 100644 index 00000000..7715cb76 --- /dev/null +++ b/plugins/modules/vyos_nat.py @@ -0,0 +1,934 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for vyos_nat +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: vyos_nat +version_added: 6.0.0 +short_description: NAT resource module +description: +- This module manages NAT configuration on devices running VyOS. +author: +- Evgeny Molotkov (@omnom62) +notes: +- Tested against VyOS 1.4.3 and 1.5.0. +- This module works with connection C(network_cli). +options: + config: + description: + - The desired configuration for the NAT resource represented as a dictionary. + type: dict + suboptions: + nat: + type: dict + description: Configuration for NAT rules. + suboptions: + cgnat: + type: dict + description: Configuration for Carrier Grade NAT (CGNAT). + suboptions: + log_allocation: + type: bool + description: Log CGNAT address allocations. + pool: + type: dict + description: Configuration for CGNAT pools. + suboptions: + external: + type: list + elements: dict + description: List of external NAT pools for CGNAT. + suboptions: + name: + type: str + required: true + description: Name of the external NAT pool. + external_port_range: + type: str + description: Port range to use for NAT translations in this external pool. + per_user_limit: + type: dict + description: Per-user limit configuration for the external pool. + suboptions: + port: + type: str + description: Maximum number of ports allocated per user. + range: + type: list + elements: dict + description: List of external IP address ranges in the pool. + suboptions: + value: + type: str + required: true + description: IP address, prefix, or range (e.g. 203.0.113.0/24 or 203.0.113.1-203.0.113.60). + seq: + type: str + description: Optional sequence number for this range entry. + internal: + type: list + elements: dict + description: List of internal NAT pools for CGNAT. + suboptions: + name: + type: str + required: true + description: Name of the internal NAT pool. + range: + type: list + elements: str + description: List of internal IP addresses or prefixes in the pool. + rule: + type: list + elements: dict + description: List of CGNAT rules. + suboptions: + id: + type: int + required: true + description: Rule number for CGNAT. + source: + type: dict + description: Source pool configuration for CGNAT translation. + suboptions: + pool: + type: str + description: Source pool name to use for CGNAT translation. + translation: + type: dict + description: Translation pool configuration for CGNAT. + suboptions: + pool: + type: str + description: Translation pool name to use for CGNAT translation. + destination: + type: dict + description: Configuration for destination NAT rules. + suboptions: + rule: + type: list + elements: dict + description: List of destination NAT rules. + suboptions: + id: + type: int + required: true + description: Rule number for destination NAT. + description: + type: str + description: User-friendly description of the destination NAT rule. + protocol: + type: str + description: Protocol to NAT (default all). + packet_type: + type: str + description: Packet type to match. + exclude: + type: bool + description: Exclude packets matching this rule from NAT. + log: + type: bool + description: Log packets hitting this rule. + disable: + type: bool + description: Disable this destination NAT rule. + inbound_interface: + type: dict + description: Match inbound interface. + suboptions: + name: + type: str + description: Interface name to match. + group: + type: str + description: Interface group to match. + destination: + type: dict + description: Match criteria for destination NAT. + suboptions: + address: + type: str + description: IP address, subnet, or range to match. + fqdn: + type: str + description: Fully qualified domain name to match. + port: + type: str + description: Port number or range to match. + address_group: + type: str + description: Address group name to match. + domain_group: + type: str + description: Domain group name to match. + mac_group: + type: str + description: MAC address group name to match. + network_group: + type: str + description: Network group name to match. + port_group: + type: str + description: Port group name to match. + translation: + type: dict + description: Translation configuration for destination NAT. + suboptions: + address: + type: str + description: IP address or prefix to translate destination to. + port: + type: str + description: Port number or range to translate destination port to. + redirect_port: + type: str + description: Redirect to local port number. + address_mapping: + type: str + choices: + - random + - persistent + description: Address mapping mode for translation. + port_mapping: + type: str + choices: + - random + - none + description: Port mapping mode for translation. + load_balance: + type: dict + description: Load balancing configuration for this NAT rule. + suboptions: + hash: + type: list + elements: str + description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address). + choices: + - source-address + - destination-address + - source-port + - destination-port + - random + backend: + type: list + elements: dict + description: List of backends to load-balance across. Weights should sum to 100. + suboptions: + ip: + type: str + description: IP address of the backend translation target. + weight: + type: int + description: Relative weight (1-100) for this backend's share of load-balanced traffic. + source: + type: dict + description: Configuration for source NAT rules. + suboptions: + rule: + type: list + elements: dict + description: List of source NAT rules. + suboptions: + id: + type: int + required: true + description: Rule number for source NAT. + description: + type: str + description: User-friendly description of the source NAT rule. + protocol: + type: str + description: Protocol to NAT (default all). + packet_type: + type: str + description: Packet type to match. + exclude: + type: bool + description: Exclude packets matching this rule from NAT. + log: + type: bool + description: Log packets hitting this rule. + disable: + type: bool + description: Disable this source NAT rule. + outbound_interface: + type: dict + description: Match outbound interface. + suboptions: + name: + type: str + description: Interface name to match. + group: + type: str + description: Interface group to match. + destination: + type: dict + description: Destination match criteria for source NAT. + suboptions: + address: + type: str + description: IP address, subnet, or range to match. + fqdn: + type: str + description: Fully qualified domain name to match. + port: + type: str + description: Port number or range to match. + address_group: + type: str + description: Address group name to match. + domain_group: + type: str + description: Domain group name to match. + mac_group: + type: str + description: MAC address group name to match. + network_group: + type: str + description: Network group name to match. + port_group: + type: str + description: Port group name to match. + source: + type: dict + description: Source match criteria for source NAT. + suboptions: + address: + type: str + description: IP address, subnet, or range to match. + fqdn: + type: str + description: Fully qualified domain name to match. + port: + type: str + description: Port number or range to match. + address_group: + type: str + description: Address group name to match. + domain_group: + type: str + description: Domain group name to match. + mac_group: + type: str + description: MAC address group name to match. + network_group: + type: str + description: Network group name to match. + port_group: + type: str + description: Port group name to match. + translation: + type: dict + description: Translation configuration for source NAT. + suboptions: + address: + type: str + description: IP address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address. + port: + type: str + description: Port number or range to translate source port to. + address_mapping: + type: str + choices: + - random + - persistent + description: Address mapping mode for translation. + port_mapping: + type: str + choices: + - random + - none + description: Port mapping mode for translation. + load_balance: + type: dict + description: Load balancing configuration for this NAT rule. + suboptions: + hash: + type: list + elements: str + description: Fields to hash on for load balancing. Mutually exclusive with I(translation.address). + choices: + - source-address + - destination-address + - source-port + - destination-port + - random + backend: + type: list + elements: dict + description: List of backends to load-balance across. Weights should sum to 100. + suboptions: + ip: + type: str + description: IP address of the backend translation target. + weight: + type: int + description: Relative weight (1-100) for this backend's share of load-balanced traffic. + static: + type: dict + description: Configuration for static one-to-one NAT rules. + suboptions: + rule: + type: list + elements: dict + description: List of static NAT rules. + suboptions: + id: + type: int + required: true + description: Rule number for static NAT. + description: + type: str + description: User-friendly description of the static NAT rule. + destination: + type: dict + description: Match criteria for static NAT. + suboptions: + address: + type: str + description: IP address, subnet, or range to match. + inbound_interface: + type: str + description: Inbound interface that this static NAT rule applies to. + log: + type: bool + description: Log packets hitting this static NAT rule. + translation: + type: dict + description: Translation configuration for static NAT. + suboptions: + address: + type: str + description: IP address or prefix to translate to. + nat64: + type: dict + description: Configuration for NAT64 (IPv6-to-IPv4) rules. + suboptions: + source: + type: dict + description: Configuration for NAT64 source rules. + suboptions: + rule: + type: list + elements: dict + description: List of NAT64 source rules. + suboptions: + id: + type: int + required: true + description: Rule number for NAT64 source rule (1-999999). + description: + type: str + description: User-friendly description of the NAT64 source rule. + disable: + type: bool + description: Disable this NAT64 source rule. + match: + type: dict + description: Match criteria for NAT64 source rule. + suboptions: + mark: + type: int + description: Match on firewall mark value (1-2147483647). + source: + type: dict + description: IPv6 source prefix to match for NAT64 translation. + suboptions: + prefix: + type: str + description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x). + translation: + type: dict + description: Translation configuration for NAT64 source rule. + suboptions: + pool: + type: list + elements: dict + description: List of translation pools for NAT64. + suboptions: + id: + type: int + required: true + description: Pool number (1-999999). + address: + type: str + description: IPv4 address or prefix for translation pool. + description: + type: str + description: User-friendly description of the translation pool. + disable: + type: bool + description: Disable this translation pool. + port: + type: str + description: Port number or range for translation pool. + protocol: + type: str + choices: + - icmp + - tcp + - udp + description: Protocol for this translation pool entry. + nat66: + type: dict + description: Configuration for NAT66 (IPv6-to-IPv6) rules. + suboptions: + destination: + type: dict + description: Configuration for NAT66 destination rules. + suboptions: + rule: + type: list + elements: dict + description: List of NAT66 destination rules. + suboptions: + id: + type: int + required: true + description: Rule number for NAT66 destination rule. + description: + type: str + description: User-friendly description of the NAT66 destination rule. + destination: + type: dict + description: Match criteria for NAT66 destination rule. + suboptions: + address: + type: str + description: IPv6 address or prefix to match. + port: + type: str + description: Port number or range to match. + disable: + type: bool + description: Disable this NAT66 destination rule. + exclude: + type: bool + description: Exclude packets matching this rule from NAT66. + inbound_interface: + type: dict + description: Inbound interface to match for NAT66 destination rule. + suboptions: + name: + type: str + description: Interface name to match. + log: + type: bool + description: Log packets hitting this NAT66 destination rule. + protocol: + type: str + description: Protocol to match. + source: + type: dict + description: Source match criteria for NAT66 destination rule. + suboptions: + address: + type: str + description: IPv6 source address or prefix to match. + port: + type: str + description: Source port number or range to match. + translation: + type: dict + description: Translation configuration for NAT66 destination rule. + suboptions: + address: + type: str + description: IPv6 address or prefix to translate destination to. + port: + type: str + description: Port number or range to translate destination port to. + source: + type: dict + description: Configuration for NAT66 source rules. + suboptions: + rule: + type: list + elements: dict + description: List of NAT66 source rules. + suboptions: + id: + type: int + required: true + description: Rule number for NAT66 source rule. + description: + type: str + description: User-friendly description of the NAT66 source rule. + destination: + type: dict + description: Destination match criteria for NAT66 source rule. + suboptions: + port: + type: str + description: Destination port number or range to match. + prefix: + type: str + description: IPv6 destination prefix to match (h:h:h:h:h:h:h:h/x). + disable: + type: bool + description: Disable this NAT66 source rule. + exclude: + type: bool + description: Exclude packets matching this rule from NAT66. + log: + type: bool + description: Log packets hitting this NAT66 source rule. + outbound_interface: + type: dict + description: Outbound interface to match for NAT66 source rule. + suboptions: + name: + type: str + description: Interface name to match. + protocol: + type: str + description: Protocol to match. + source: + type: dict + description: Source match criteria for NAT66 source rule. + suboptions: + port: + type: str + description: Source port number or range to match. + prefix: + type: str + description: IPv6 source prefix to match (h:h:h:h:h:h:h:h/x). + translation: + type: dict + description: Translation configuration for NAT66 source rule. + suboptions: + address: + type: str + description: IPv6 address or prefix to translate source to. Use masquerade to masquerade as the outbound interface address. + port: + type: str + description: Port number or range to translate source port to. + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the VyOS device by + executing the command B(show configuration commands | match 'nat'). + - The state I(parsed) reads the configuration from C(show configuration commands | match 'nat') + and transforms it into Ansible structured data as per the module argspec. + The value is then returned in the I(parsed) key within the result. + - The state I(replaced) replaces only the provided configuration, while I(overridden) removes any + existing NAT configuration not specified in I(config). + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" +EXAMPLES = """ +# Using merged - configure CGNAT +- name: Merge CGNAT configuration + vyos.vyos.vyos_nat: + config: + nat: + cgnat: + log_allocation: true + pool: + external: + - name: ext-pool-1 + external_port_range: "10000-20000" + per_user_limit: + port: "200" + range: + - value: 203.0.113.0/24 + internal: + - name: int-pool-1 + range: + - 10.0.0.0/24 + rule: + - id: 1 + source: + pool: int-pool-1 + translation: + pool: ext-pool-1 + state: merged + +# Using merged - configure destination NAT +- name: Merge destination NAT rule + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Web server NAT" + protocol: tcp + log: true + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: merged + +# Using merged - configure source NAT +- name: Merge source NAT rule + vyos.vyos.vyos_nat: + config: + nat: + source: + rule: + - id: 200 + description: "Outbound masquerade" + protocol: tcp + log: true + outbound_interface: + name: eth0 + translation: + address: masquerade + state: merged + +# Using merged - configure static NAT +- name: Merge static NAT rule + vyos.vyos.vyos_nat: + config: + nat: + static: + rule: + - id: 300 + description: "Static mapping" + inbound_interface: eth2 + destination: + address: 198.51.100.20 + translation: + address: 192.168.1.20 + log: true + state: merged + +# Using merged - configure NAT64 +- name: Merge NAT64 source rule + vyos.vyos.vyos_nat: + config: + nat64: + source: + rule: + - id: 10 + description: "NAT64 example" + source: + prefix: 2001:db8::/96 + match: + mark: 100 + translation: + pool: + - id: 1 + address: 192.168.100.10 + port: "1-65535" + protocol: udp + state: merged + +# Using merged - configure NAT66 +- name: Merge NAT66 destination rule + vyos.vyos.vyos_nat: + config: + nat66: + destination: + rule: + - id: 20 + description: "NAT66 DNAT" + protocol: tcp + inbound_interface: + name: eth1 + destination: + address: 2001:db8::1 + translation: + address: 2001:db8:1::10 + port: "8443" + state: merged + +# Using replaced - replace specific NAT rules +- name: Replace destination NAT rule + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Replaced web server NAT" + protocol: tcp + destination: + address: 198.51.100.10 + port: "443" + translation: + address: 192.168.1.10 + port: "8443" + state: replaced + +# Using overridden - override entire NAT configuration +- name: Override entire NAT configuration + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Only rule after override" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: overridden + +# Using deleted - delete all NAT configuration +- name: Delete all NAT configuration + vyos.vyos.vyos_nat: + state: deleted + +# Using deleted - delete specific NAT rules +- name: Delete specific NAT rules + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + source: + rule: + - id: 200 + nat64: + source: + rule: + - id: 10 + state: deleted + +# Using gathered +- name: Gather NAT configuration from device + vyos.vyos.vyos_nat: + state: gathered + +# Using rendered +- name: Render NAT configuration offline + vyos.vyos.vyos_nat: + config: + nat: + destination: + rule: + - id: 100 + description: "Rendered rule" + protocol: tcp + destination: + address: 198.51.100.10 + port: "80" + translation: + address: 192.168.1.10 + port: "8080" + state: rendered + +# Using parsed +- name: Parse NAT configuration from file + vyos.vyos.vyos_nat: + running_config: "{{ lookup('file', './nat_config.cfg') }}" + state: parsed +""" +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + 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) or C(deleted) + type: list + sample: + - set nat destination rule 100 description 'Web server NAT' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 inbound-interface name eth2 + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 translation address 192.168.1.10 + - delete nat source rule 200 +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 nat destination rule 100 description 'Web server NAT' + - set nat destination rule 100 protocol tcp + - set nat destination rule 100 inbound-interface name eth2 + - set nat destination rule 100 destination address 198.51.100.10 + - set nat destination rule 100 translation address 192.168.1.10 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict + 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: dict + 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.nat.nat import ( + NatArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.nat.nat import ( + Nat, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=NatArgs.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 = Nat(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_ntp_global.py b/plugins/modules/vyos_ntp_global.py index cad08a68..ae1330ff 100644 --- a/plugins/modules/vyos_ntp_global.py +++ b/plugins/modules/vyos_ntp_global.py @@ -10,7 +10,6 @@ The module file for vyos_ntp_global from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -22,7 +21,7 @@ description: author: - Varshitha Yataluru (@YVarshitha) notes: -- Tested against vyos 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(network_cli). - "VyOS v.1.4+ uses chronyd, and path changes from `system` to `service`" options: diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py index f86acb7a..d49e9d35 100644 --- a/plugins/modules/vyos_ospf_interfaces.py +++ b/plugins/modules/vyos_ospf_interfaces.py @@ -10,7 +10,6 @@ The module file for vyos_ospf_interfaces from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -19,6 +18,7 @@ version_added: 1.2.0 short_description: OSPF Interfaces Resource Module. description: - This module manages OSPF configuration of interfaces on devices running VYOS. +- The provided examples of commands are valid for VyOS 1.4+ author: Gomathi Selvi Srinivasan (@GomathiselviS) options: config: @@ -173,14 +173,14 @@ EXAMPLES = """ # -------------- # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' # "after": [ # " @@ -244,14 +244,14 @@ EXAMPLES = """ # ], # "changed": true, # "commands": [ -# "set interfaces ethernet eth1 ip ospf transmit-delay 50", -# "set interfaces ethernet eth1 ip ospf priority 26", -# "set interfaces ethernet eth1 ip ospf network point-to-point", -# "set interfaces ethernet eth1 ipv6 ospfv3 dead-interval 39", -# "set interfaces bonding bond2 ip ospf transmit-delay 45", -# "set interfaces bonding bond2 ip ospf bandwidth 70", -# "set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key 1111111111232345", -# "set interfaces bonding bond2 ipv6 ospfv3 passive" +# "set protocols ospf interface eth1 transmit-delay 50", +# "set protocols ospf interface eth1 priority 26", +# "set protocols ospf interface eth1 network point-to-point", +# "set protocols ospfv3 interface eth1 dead-interval 39", +# "set protocols ospf interface bond2 transmit-delay 45", +# "set protocols ospf interface bond2 bandwidth 70", +# "set protocols ospf interface bond2 authentication md5 key-id 10 md5-key 1111111111232345", +# "set protocols ospfv3 interface bond2 passive" # ], # Using replaced: @@ -260,14 +260,14 @@ EXAMPLES = """ # ------------ # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' - name: Replace provided configuration with device configuration vyos.vyos.vyos_ospf_interfaces: @@ -290,10 +290,10 @@ EXAMPLES = """ # ----------- # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf cost '100' -# set interfaces ethernet eth1 ipv6 ospfv3 ifmtu '33' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 cost '100' +# set protocols ospfv3 interface eth1 ifmtu '33' # vyos@vyos:~$ # Module Execution @@ -383,14 +383,14 @@ EXAMPLES = """ # ], # "changed": true, # "commands": [ -# "set interfaces ethernet eth1 ip ospf cost 100", -# "set interfaces ethernet eth1 ipv6 ospfv3 ifmtu 33", -# "delete interfaces ethernet eth1 ip ospf network point-to-point", -# "delete interfaces ethernet eth1 ip ospf priority 26", -# "delete interfaces ethernet eth1 ip ospf transmit-delay 50", -# "delete interfaces ethernet eth1 ipv6 ospfv3 dead-interval 39", -# "delete interfaces bonding bond2 ip ospf authentication", -# "delete interfaces bonding bond2 ip ospf bandwidth 70" +# "set protocols ospf interface eth1 cost 100", +# "set protocols ospfv3 interface eth1 ifmtu 33", +# "delete protocols ospf interface eth1 network point-to-point", +# "delete protocols ospf interface eth1 priority 26", +# "delete protocols ospf interface eth1 transmit-delay 50", +# "delete protocols ospfv3 interface eth1 dead-interval 39", +# "delete protocols ospf interface bond2 authentication", +# "delete protocols ospf interface bond2 bandwidth 70" # ], # @@ -401,16 +401,16 @@ EXAMPLES = """ # ------------ # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf cost '100' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' -# set interfaces ethernet eth1 ipv6 ospfv3 ifmtu '33' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 cost '100' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' +# set protocols ospfv3 interface eth1 ifmtu '33' # vyos@vyos:~$ - name: Override device configuration with provided configuration @@ -429,9 +429,9 @@ EXAMPLES = """ # ----------- # 200~vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces ethernet eth0 ip ospf cost '100' -# set interfaces ethernet eth0 ipv6 ospfv3 ifmtu '33' -# set interfaces ethernet eth0 ipv6 ospfv3 'passive' +# set protocols ospf interface eth0 cost '100' +# set protocols ospfv3 interface eth0 ifmtu '33' +# set protocols ospfv3 interface eth0 'passive' # vyos@vyos:~$ # # @@ -513,13 +513,13 @@ EXAMPLES = """ # ], # "changed": true, # "commands": [ -# "delete interfaces bonding bond2 ip ospf", -# "delete interfaces bonding bond2 ipv6 ospfv3", -# "delete interfaces ethernet eth1 ip ospf", -# "delete interfaces ethernet eth1 ipv6 ospfv3", -# "set interfaces ethernet eth0 ip ospf cost 100", -# "set interfaces ethernet eth0 ipv6 ospfv3 ifmtu 33", -# "set interfaces ethernet eth0 ipv6 ospfv3 passive" +# "delete protocols ospf interface bond2", +# "delete protocols ospfv3 interface bond2", +# "delete protocols ospf interface eth1", +# "delete protocols ospfv3 interface eth1", +# "set protocols ospf interface eth0 cost 100", +# "set protocols ospfv3 interface eth0 ifmtu 33", +# "set protocols ospfv3 interface eth0 passive" # ], # @@ -530,17 +530,17 @@ EXAMPLES = """ # ------------- # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth0 ip ospf cost '100' -# set interfaces ethernet eth0 ipv6 ospfv3 ifmtu '33' -# set interfaces ethernet eth0 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth0 cost '100' +# set protocols ospfv3 interface eth0 ifmtu '33' +# set protocols ospfv3 interface eth0 'passive' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' # vyos@vyos:~$ - name: Delete device configuration @@ -553,14 +553,14 @@ EXAMPLES = """ # ----------- # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' # vyos@vyos:~$ # # @@ -669,25 +669,25 @@ EXAMPLES = """ # ], # "changed": true, # "commands": [ -# "delete interfaces ethernet eth0 ip ospf", -# "delete interfaces ethernet eth0 ipv6 ospfv3" +# "delete protocols ospf interface eth0", +# "delete protocols ospfv3 interface eth0" # ], # # Using parsed: # parsed.cfg: -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth0 ip ospf cost '50' -# set interfaces ethernet eth0 ip ospf priority '26' -# set interfaces ethernet eth0 ipv6 ospfv3 instance-id '33' -# set interfaces ethernet eth0 ipv6 ospfv3 'mtu-ignore' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth0 cost '50' +# set protocols ospf interface eth0 priority '26' +# set protocols ospfv3 interface eth0 instance-id '33' +# set protocols ospfv3 interface eth0 'mtu-ignore' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' # - name: parse configs @@ -782,14 +782,14 @@ EXAMPLES = """ # ---------------- # "rendered": [ -# "set interfaces ethernet eth1 ip ospf transmit-delay 50", -# "set interfaces ethernet eth1 ip ospf priority 26", -# "set interfaces ethernet eth1 ip ospf network point-to-point", -# "set interfaces ethernet eth1 ipv6 ospfv3 dead-interval 39", -# "set interfaces bonding bond2 ip ospf transmit-delay 45", -# "set interfaces bonding bond2 ip ospf bandwidth 70", -# "set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key 1111111111232345", -# "set interfaces bonding bond2 ipv6 ospfv3 passive" +# "set protocols ospf interface eth1 transmit-delay 50", +# "set protocols ospf interface eth1 priority 26", +# "set protocols ospf interface eth1 network point-to-point", +# "set protocols ospfv3 interface eth1 dead-interval 39", +# "set protocols ospf interface bond2 transmit-delay 45", +# "set protocols ospf interface bond2 bandwidth 70", +# "set protocols ospf interface bond2 authentication md5 key-id 10 md5-key 1111111111232345", +# "set protocols ospfv3 interface bond2 passive" # ] # @@ -799,14 +799,14 @@ EXAMPLES = """ # Native Config: # vyos@vyos:~$ show configuration commands | match "ospf" -# set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345' -# set interfaces bonding bond2 ip ospf bandwidth '70' -# set interfaces bonding bond2 ip ospf transmit-delay '45' -# set interfaces bonding bond2 ipv6 ospfv3 'passive' -# set interfaces ethernet eth1 ip ospf network 'point-to-point' -# set interfaces ethernet eth1 ip ospf priority '26' -# set interfaces ethernet eth1 ip ospf transmit-delay '50' -# set interfaces ethernet eth1 ipv6 ospfv3 dead-interval '39' +# set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345' +# set protocols ospf interface bond2 bandwidth '70' +# set protocols ospf interface bond2 transmit-delay '45' +# set protocols ospfv3 interface bond2 'passive' +# set protocols ospf interface eth1 network 'point-to-point' +# set protocols ospf interface eth1 priority '26' +# set protocols ospf interface eth1 transmit-delay '50' +# set protocols ospfv3 interface eth1 dead-interval '39' # vyos@vyos:~$ - name: gather configs @@ -884,17 +884,17 @@ commands: returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) type: list sample: - - "set interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345'" - - "set interfaces bonding bond2 ip ospf bandwidth '70'" - - "set interfaces bonding bond2 ip ospf transmit-delay '45'" + - "set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345'" + - "set protocols ospf interface bond2 bandwidth '70'" + - "set protocols ospf interface bond2 transmit-delay '45'" 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 interfaces bonding bond2 ip ospf authentication md5 key-id 10 md5-key '1111111111232345'" - - "set interfaces bonding bond2 ip ospf bandwidth '70'" - - "set interfaces bonding bond2 ip ospf transmit-delay '45'" + - "set protocols ospf interface bond2 authentication md5 key-id 10 md5-key '1111111111232345'" + - "set protocols ospf interface bond2 bandwidth '70'" + - "set protocols ospf interface bond2 transmit-delay '45'" gathered: description: Facts about the network resource gathered from the remote device as structured data. returned: when I(state) is C(gathered) diff --git a/plugins/modules/vyos_ospfv2.py b/plugins/modules/vyos_ospfv2.py index a72b7fd2..85822e89 100644 --- a/plugins/modules/vyos_ospfv2.py +++ b/plugins/modules/vyos_ospfv2.py @@ -28,7 +28,6 @@ The module file for vyos_ospfv2 from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -45,7 +44,8 @@ short_description: OSPFv2 resource module description: This resource module configures and manages attributes of OSPFv2 routes on VyOS network devices. notes: -- Tested against VyOS 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 +- The provided examples of commands are valid for VyOS 1.4+ - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: @@ -456,8 +456,8 @@ EXAMPLES = """ # "set protocols ospf parameters opaque-lsa", # "set protocols ospf parameters abr-type 'cisco'", # "set protocols ospf parameters rfc1583-compatibility", -# "set protocols ospf passive-interface eth1", -# "set protocols ospf passive-interface eth2", +# "set protocols ospf interface 'eth1' passive", +# "set protocols ospf interface 'eth2' passive", # "set protocols ospf max-metric router-lsa on-shutdown 10", # "set protocols ospf max-metric router-lsa administrative", # "set protocols ospf max-metric router-lsa on-startup 10", @@ -601,8 +601,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' @@ -864,8 +864,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' @@ -901,8 +901,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' # @@ -1059,7 +1059,7 @@ EXAMPLES = """ # } # # "commands": [ -# "delete protocols ospf passive-interface eth2", +# "delete protocols ospf interface 'eth2' passive", # "delete protocols ospf area 3", # "delete protocols ospf area 4 range 192.0.3.0/24 cost", # "delete protocols ospf area 4 range 192.0.3.0/24", @@ -1191,7 +1191,7 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' +# set protocols ospf interface 'eth1' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' @@ -1279,8 +1279,8 @@ EXAMPLES = """ # "set protocols ospf parameters opaque-lsa", # "set protocols ospf parameters abr-type 'cisco'", # "set protocols ospf parameters rfc1583-compatibility", -# "set protocols ospf passive-interface eth1", -# "set protocols ospf passive-interface eth2", +# "set protocols ospf interface 'eth1' passive", +# "set protocols ospf interface 'eth2' passive", # "set protocols ospf max-metric router-lsa on-shutdown 10", # "set protocols ospf max-metric router-lsa administrative", # "set protocols ospf max-metric router-lsa on-startup 10", @@ -1335,8 +1335,8 @@ EXAMPLES = """ set protocols ospf parameters 'opaque-lsa' set protocols ospf parameters 'rfc1583-compatibility' set protocols ospf parameters router-id '192.0.1.1' - set protocols ospf passive-interface 'eth1' - set protocols ospf passive-interface 'eth2' + set protocols ospf interface 'eth1' passive + set protocols ospf interface 'eth2' passive set protocols ospf redistribute bgp metric '10' set protocols ospf redistribute bgp metric-type '2' state: parsed @@ -1472,8 +1472,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' # @@ -1608,8 +1608,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' @@ -1645,8 +1645,8 @@ EXAMPLES = """ # set protocols ospf parameters 'opaque-lsa' # set protocols ospf parameters 'rfc1583-compatibility' # set protocols ospf parameters router-id '192.0.1.1' -# set protocols ospf passive-interface 'eth1' -# set protocols ospf passive-interface 'eth2' +# set protocols ospf interface 'eth1' passive +# set protocols ospf interface 'eth2' passive # set protocols ospf redistribute bgp metric '10' # set protocols ospf redistribute bgp metric-type '2' # @@ -1781,7 +1781,7 @@ commands: type: list sample: - "set protocols ospf parameters router-id 192.0.1.1" - - "set protocols ospf passive-interface 'eth1'" + - "set protocols ospf interface 'eth1' passive" """ diff --git a/plugins/modules/vyos_ospfv3.py b/plugins/modules/vyos_ospfv3.py index 81b26327..89a5ab24 100644 --- a/plugins/modules/vyos_ospfv3.py +++ b/plugins/modules/vyos_ospfv3.py @@ -28,7 +28,6 @@ The module file for vyos_ospfv3 from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -46,7 +45,7 @@ description: This resource module configures and manages attributes of OSPFv3 ro author: - Rohit Thakur (@rohitthakur2590) notes: -- Tested against VyOS 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: @@ -68,6 +67,15 @@ options: import_list: description: Name of import-list. type: str + interface: + description: Enable OSPVv3 on an interface for this area. + aliases: ['interfaces'] + type: list + elements: dict + suboptions: + name: + description: Interface name. + type: str range: description: Summarize routes matching prefix (border routers only). type: list diff --git a/plugins/modules/vyos_ping.py b/plugins/modules/vyos_ping.py index 98619399..1e81111e 100644 --- a/plugins/modules/vyos_ping.py +++ b/plugins/modules/vyos_ping.py @@ -21,7 +21,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -30,7 +29,7 @@ module: vyos_ping short_description: Tests reachability using ping from VyOS network devices description: - Tests reachability using ping from a VyOS device to a remote destination. -- Tested against VyOS 1.1.8 (helium) +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - For a general purpose network module, see the M(ansible.netcommon.net_ping) module. - For Windows targets, use the M(ansible.windows.win_ping) module instead. - For targets running Python, use the M(ansible.builtin.ping) module instead. @@ -73,7 +72,7 @@ options: - present default: present notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - For a general purpose network module, see the M(ansible.netcommon.net_ping) module. - For Windows targets, use the M(ansible.windows.win_ping) module instead. - For targets running Python, use the M(ansible.builtin.ping) module instead. diff --git a/plugins/modules/vyos_prefix_lists.py b/plugins/modules/vyos_prefix_lists.py index 71d52b32..b9b5ca99 100644 --- a/plugins/modules/vyos_prefix_lists.py +++ b/plugins/modules/vyos_prefix_lists.py @@ -10,7 +10,6 @@ The module file for vyos_prefix_lists from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -21,7 +20,7 @@ description: version_added: 2.4.0 author: Priyam Sahoo (@priyamsahoo) notes: - - Tested against VyOS 1.1.8 (helium) + - Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(network_cli) options: config: diff --git a/plugins/modules/vyos_route_maps.py b/plugins/modules/vyos_route_maps.py index 67d327a6..8bd55f3d 100644 --- a/plugins/modules/vyos_route_maps.py +++ b/plugins/modules/vyos_route_maps.py @@ -10,7 +10,6 @@ The module file for vyos_route_maps from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -21,7 +20,7 @@ description: - This module manages route map configurations on devices running VYOS. author: Ashwini Mhatre (@amhatre) notes: -- Tested against vyos 1.3.8 +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025 - This module works with connection C(network_cli). options: config: @@ -103,6 +102,12 @@ options: extcommunity_soo: type: str description: Set Site of Origin value. ASN:nn_or_IP_address:nn VPN extended community + extcommunity_bandwidth: + type: str + description: Set Bandwidth of Origin value. 1-25600|cumulative|num-multipaths VPN extended community + extcommunity_bandwidth_non_transitive: + type: bool + description: Set the bandwidth extended community encoded as non-transitive True/False VPN extended community ip_next_hop: type: str description: IP address. @@ -146,6 +151,9 @@ options: weight: type: str description: Border Gateway Protocol (BGP) weight attribute. Example <0-4294967295> + table: + type: str + description: Set prefixes to table. Example <1-200> match: description: Route parameters to match. type: dict @@ -226,6 +234,10 @@ options: type: str description: RPKI validation value. choices: [ "notfound", "invalid", "valid" ] + protocol: + type: str + description: Source protocol to match. + choices: [ "babel","bgp","connected","isis","kernel","ospf","ospfv3","rip","ripng","static","table","vnc" ] on_match: type: dict description: Exit policy on matches. diff --git a/plugins/modules/vyos_snmp_server.py b/plugins/modules/vyos_snmp_server.py index f574919a..a72fb266 100644 --- a/plugins/modules/vyos_snmp_server.py +++ b/plugins/modules/vyos_snmp_server.py @@ -10,7 +10,6 @@ The module file for vyos_snmp_server from __future__ import absolute_import, division, print_function - __metaclass__ = type DOCUMENTATION = """ @@ -20,7 +19,7 @@ short_description: Manages snmp_server resource module description: This module manages the snmp server attributes of Vyos network devices author: Gomathi Selvi Srinivasan (@GomathiselviS) notes: - - Tested against vyos 1.3.8, 1.4.1 + - Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025, 1.4.1 - This module works with connection C(network_cli). - The Configuration defaults of the Vyos network devices are supposed to hinder idempotent behavior of plays diff --git a/plugins/modules/vyos_static_routes.py b/plugins/modules/vyos_static_routes.py index 0629a8bd..37593ffa 100644 --- a/plugins/modules/vyos_static_routes.py +++ b/plugins/modules/vyos_static_routes.py @@ -28,7 +28,6 @@ The module file for vyos_static_routes from __future__ import absolute_import, division, print_function - __metaclass__ = type ANSIBLE_METADATA = { @@ -44,7 +43,7 @@ version_added: '1.0.0' short_description: Static routes resource module description: This module manages attributes of static routes on VyOS network devices. notes: -- Tested against VyOS 1.3.8. +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). author: diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py index 96a0e9bc..25b32dd7 100644 --- a/plugins/modules/vyos_system.py +++ b/plugins/modules/vyos_system.py @@ -16,8 +16,8 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see <http://www.gnu.org/licenses/>. # -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -33,7 +33,7 @@ version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: host_name: @@ -94,21 +94,27 @@ EXAMPLES = """ - sub1.example.com - sub2.example.com """ +from re import M, findall from ansible.module_utils.basic import AnsibleModule +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_config, + get_os_version, load_config, ) -def spec_key_to_device_key(key): +def spec_key_to_device_key(key, module): device_key = key.replace("_", "-") - # domain-search is longer than just it's key + # domain-search differs in 1.3- and 1.4+ if device_key == "domain-search": - device_key += " domain" + if LooseVersion(get_os_version(module)) <= LooseVersion("1.3"): + device_key += " domain" return device_key @@ -119,19 +125,20 @@ def config_to_dict(module): config = {"domain_search": [], "name_server": []} for line in data.split("\n"): - if line.startswith("set system host-name"): - config["host_name"] = line[22:-1] - elif line.startswith("set system domain-name"): - config["domain_name"] = line[24:-1] - elif line.startswith("set system domain-search domain"): - config["domain_search"].append(line[33:-1]) - elif line.startswith("set system name-server"): - config["name_server"].append(line[24:-1]) - + config_line = findall(r"^set system\s+(\S+)(?:\s+domain)?\s+'([^']+)'", line, M) + if config_line: + if config_line[0][0] == "host-name": + config["host_name"] = config_line[0][1] + elif config_line[0][0] == "domain-name": + config["domain_name"] = config_line[0][1] + elif config_line[0][0] == "domain-search": + config["domain_search"].append(config_line[0][1]) + elif config_line[0][0] == "name-server": + config["name_server"].append(config_line[0][1]) return config -def spec_to_commands(want, have): +def spec_to_commands(want, have, module): commands = [] state = want.pop("state") @@ -140,7 +147,7 @@ def spec_to_commands(want, have): if state == "absent" and all(v is None for v in want.values()): # Clear everything for key in have: - commands.append("delete system %s" % spec_key_to_device_key(key)) + commands.append("delete system %s" % spec_key_to_device_key(key, module)) for key in want: if want[key] is None: @@ -148,7 +155,7 @@ def spec_to_commands(want, have): current = have.get(key) proposed = want[key] - device_key = spec_key_to_device_key(key) + device_key = spec_key_to_device_key(key, module) # These keys are lists which may need to be reconciled with the device if key in ["domain_search", "name_server"]: @@ -201,7 +208,7 @@ def main(): want = map_param_to_obj(module) have = config_to_dict(module) - commands = spec_to_commands(want, have) + commands = spec_to_commands(want, have, module) result["commands"] = commands if commands: diff --git a/plugins/modules/vyos_user.py b/plugins/modules/vyos_user.py index 5aebf943..7c21b074 100644 --- a/plugins/modules/vyos_user.py +++ b/plugins/modules/vyos_user.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function - __metaclass__ = type # (c) 2017, Ansible by Red Hat, inc @@ -37,7 +36,7 @@ version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: aggregate: @@ -55,7 +54,7 @@ options: description: - The username to be configured on the VyOS device. This argument accepts a string value and is mutually exclusive with the C(aggregate) argument. - required: True + required: true type: str full_name: description: @@ -118,6 +117,8 @@ options: - ecdsa-sha2-nistp384 - ssh-ed25519 - ecdsa-sha2-nistp521 + - sk-ecdsa-sha2-nistp256@openssh.com + - sk-ssh-ed25519@openssh.com name: description: @@ -205,12 +206,12 @@ commands: """ import re +import shlex from copy import deepcopy from functools import partial from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( remove_default_spec, ) @@ -277,7 +278,8 @@ def spec_to_commands(updates, module): add( commands, want, - "authentication plaintext-password %s" % want["configured_password"], + "authentication plaintext-password %s" + % shlex.quote(want["configured_password"]), ) return commands @@ -364,6 +366,8 @@ def get_param_value(key, item, module): # if key doesn't exist in the item, get it from module.params if not item.get(key): value = module.params[key] + else: + value = item.get(key) # validate the param value (if validator func exists) validator = globals().get("validate_%s" % key) @@ -424,7 +428,7 @@ def update_objects(want, have): if item is None: updates.append((entry, {})) elif item: - for key, value in iteritems(entry): + for key, value in entry.items(): if value and value != item[key]: updates.append((entry, item)) return updates @@ -445,6 +449,8 @@ def main(): "ecdsa-sha2-nistp384", "ssh-ed25519", "ecdsa-sha2-nistp521", + "sk-ecdsa-sha2-nistp256@openssh.com", + "sk-ssh-ed25519@openssh.com", ], ), ) @@ -453,7 +459,8 @@ def main(): full_name=dict(), configured_password=dict(no_log=True), encrypted_password=dict(no_log=False), - update_password=dict(default="always", choices=["on_create", "always"]), + # Explicit no_log=False: unset no_log triggers Ansible PASSWORD_MATCH on *password* names. + update_password=dict(default="always", choices=["on_create", "always"], no_log=False), state=dict(default="present", choices=["present", "absent"]), public_keys=dict(type="list", elements="dict", options=public_key_spec), ) diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py index 49cc1258..d2e004f8 100644 --- a/plugins/modules/vyos_vlan.py +++ b/plugins/modules/vyos_vlan.py @@ -6,7 +6,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -18,7 +17,7 @@ description: - This module provides declarative management of VLANs on VyOS network devices. version_added: 1.0.0 notes: -- Tested against VyOS 1.1.8 (helium). +- Tested against VyOS 1.3.8, 1.4.2, the upcoming 1.5, and the rolling release of spring 2025. - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: name: @@ -279,15 +278,11 @@ def map_config_to_obj(module): obj = {} eth = splitted_line[0].strip("'") - if eth.startswith("eth"): + if eth.startswith("eth") and "." in eth: obj["interfaces"] = [] - if "." in eth: - interface = eth.split(".")[0] - obj["interfaces"].append(interface) - obj["vlan_id"] = eth.split(".")[-1] - else: - obj["interfaces"].append(eth) - obj["vlan_id"] = None + interface = eth.split(".")[0] + obj["interfaces"].append(interface) + obj["vlan_id"] = eth.split(".")[-1] if splitted_line[1].strip("'") != "-": obj["address"] = splitted_line[1].strip("'") diff --git a/plugins/modules/vyos_vpn_ipsec.py b/plugins/modules/vyos_vpn_ipsec.py new file mode 100644 index 00000000..9af12ff7 --- /dev/null +++ b/plugins/modules/vyos_vpn_ipsec.py @@ -0,0 +1,454 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: vyos_vpn_ipsec +short_description: Manages global IPsec (ike-group, esp-group, profile, authentication, options) attributes of VyOS network devices. +description: This module manages global VPN IPsec configuration on VyOS devices + -- IKE groups, ESP groups, PSK/PPK authentication, IPsec profiles, and global + options. Site-to-site peers and IKEv2 remote-access connections are handled by + separate modules. +version_added: 1.0.0 +author: Evgeny Molotkov (@omnom62) +extends_documentation_fragment: + - vyos.vyos.vyos +notes: + - Tested against VyOS 1.4 and 1.5. + - "Source of truth for field types/choices: device node.def templates under /opt/vyatta/share/vyatta-cfg/templates/vpn/ipsec/." +options: + config: + description: IPsec global configuration. + type: dict + suboptions: + ike_group: + description: List of IKE groups. + type: list + elements: dict + suboptions: + name: + description: The name of the IKE group. + type: str + required: true + close_action: + description: Action to take if a child SA is unexpectedly closed. + type: str + choices: [none, trap, start] + dead_peer_detection: + description: Dead Peer Detection (DPD). + type: dict + suboptions: + action: + description: Keep-alive failure action. + type: str + choices: [trap, clear, restart] + interval: + description: Keep-alive interval in seconds. + type: int + timeout: + description: Dead Peer Detection keep-alive timeout (IKEv1 only), in seconds. + type: int + disable_mobike: + description: Disable MOBIKE support (IKEv2 only). + type: bool + ikev2_reauth: + description: Re-authentication of the remote peer during an IKE re-key (IKEv2 only). + type: bool + key_exchange: + description: IKE version. + type: str + choices: [ikev1, ikev2] + lifetime: + description: IKE lifetime in seconds. + type: int + mode: + description: IKEv1 phase 1 mode. + type: str + choices: [main, aggressive] + proposal: + description: List of IKE proposals. + type: list + elements: dict + suboptions: + proposal_id: + description: The proposal identifier. + type: int + dh_group: + description: Diffie-Hellman group. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side, not enumerated here since + the set is version-dependent. + type: int + encryption: + description: Encryption algorithm. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side, not enumerated here since + the set is version-dependent. + type: str + hash: + description: Hash algorithm. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side. + type: str + prf: + description: Pseudo-Random Function. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side. + type: str + esp_group: + description: List of ESP groups. + type: list + elements: dict + suboptions: + name: + description: The name of the ESP group. + type: str + required: true + compression: + description: Enable ESP compression. + type: bool + disable_rekey: + description: Do not locally initiate a re-key of the SA; remote peer must re-key before expiration. + type: bool + life_bytes: + description: Security Association byte count to expire. + type: int + life_packets: + description: Security Association packet count to expire. + type: int + lifetime: + description: Security Association time to expire, in seconds. + type: int + mode: + description: ESP mode. + type: str + choices: [tunnel, transport] + pfs: + description: ESP Perfect Forward Secrecy. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side, not enumerated here since + the set is version-dependent. + type: str + proposal: + description: List of ESP proposals. + type: list + elements: dict + suboptions: + proposal_id: + description: The proposal identifier. + type: int + encryption: + description: Encryption algorithm. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side, not enumerated here since + the set is version-dependent. + type: str + hash: + description: Hash algorithm. See VyOS/strongSwan documentation for the + full set of valid values -- validated device-side. + type: str + authentication: + description: Global pre-shared-key and post-quantum pre-shared-key definitions. + type: dict + suboptions: + psk: + description: List of pre-shared keys. + type: list + elements: dict + suboptions: + name: + description: Pre-shared key name. + type: str + required: true + id: + description: ID(s) for authentication. + type: list + elements: str + dhcp_interface: + description: DHCP interface(s) supplying next-hop IP address. + type: list + elements: str + secret: + description: IKE pre-shared secret key. + type: str + secret_type: + description: Secret encoding type. + type: str + choices: [base64, hex, plaintext] + ppk: + description: List of post-quantum pre-shared keys. + type: list + elements: dict + suboptions: + name: + description: Post-quantum pre-shared key name. + type: str + required: true + id: + description: ID(s) for PPK. + type: list + elements: str + secret: + description: Post-quantum pre-shared secret key. + type: str + secret_type: + description: Secret encoding type. + type: str + choices: [base64, hex, plaintext] + profile: + description: List of VPN IPsec profiles (used for e.g. DMVPN/GRE tunnel binding). + type: list + elements: dict + suboptions: + name: + description: Profile name. + type: str + required: true + authentication: + description: Authentication settings for this profile. + type: dict + suboptions: + mode: + description: Authentication mode. + type: str + choices: [pre-shared-secret] + pre_shared_secret: + description: Pre-shared secret key. + type: str + bind_tunnel: + description: Tunnel interface(s) associated with this profile. + type: list + elements: str + disable: + description: Disable this profile. + type: bool + esp_group: + description: ESP group name to use for this profile. + type: str + ike_group: + description: IKE group name to use for this profile. + type: str + interface: + description: Interface(s) IPsec listens on. If omitted, listens on all interfaces. + type: list + elements: str + log: + description: IPsec logging settings. + type: dict + suboptions: + level: + description: Global IPsec logging level. + type: int + subsystem: + description: Per-subsystem logging levels to enable. + type: list + elements: str + options: + description: Global IPsec options. + type: dict + suboptions: + disable_route_autoinstall: + description: Do not automatically install routes to remote networks. + type: bool + flexvpn: + description: Allow FlexVPN vendor ID payload (IKEv2 only). + type: bool + interface: + description: Single interface for IPsec options scope (distinct from top-level interface list). + type: str + retransmission: + description: IPsec retransmission settings. + type: dict + suboptions: + attempts: + description: Maximum number of retransmissions. + type: int + base: + description: Base of exponential backoff. + type: float + timeout: + description: Timeout in seconds before the first retransmission. + type: int + virtual_ip: + description: Allow install of virtual-ip addresses. + type: bool + disable_uniqreqids: + description: Disable requirement for unique IDs in the Security Database. + type: bool + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the VyOS device by + executing the command B(show configuration commands | match "vpn ipsec"). + - The states I(replaced) and I(overridden) have identical behaviour for this module + with respect to named collections (ike_group, esp_group, profile, authentication), + but differ in scope -- see the module description for detail. + - The state I(parsed) reads the configuration from the C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec, + returned in the I(parsed) key within the result. + type: str + state: + description: The state the configuration should be left in. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +""" + +EXAMPLES = """ +- name: Merge provided configuration with device configuration + vyos.vyos.vyos_vpn_ipsec: + config: + esp_group: + - name: ESP-TEST + proposal: + - proposal_id: 1 + encryption: aes256 + hash: sha256 + ike_group: + - name: IKE-TEST + key_exchange: ikev2 + proposal: + - proposal_id: 1 + encryption: aes256 + hash: sha256 + dh_group: 14 + state: merged + +- name: Replace one named esp-group, leaving all other groups untouched + vyos.vyos.vyos_vpn_ipsec: + config: + esp_group: + - name: ESP-TEST + proposal: + - proposal_id: 1 + encryption: aes128 + hash: sha256 + state: replaced + +- name: Override the whole configuration -- anything not listed here is removed + vyos.vyos.vyos_vpn_ipsec: + config: + esp_group: + - name: ESP-TEST + proposal: + - proposal_id: 1 + encryption: aes256 + hash: sha256 + state: overridden + +- name: Delete one named esp-group, leaving all other groups untouched + vyos.vyos.vyos_vpn_ipsec: + config: + esp_group: + - name: ESP-TEST + state: deleted + +- name: Remove all vpn_ipsec configuration + vyos.vyos.vyos_vpn_ipsec: + state: deleted + +- name: Gather current vpn_ipsec configuration + vyos.vyos.vyos_vpn_ipsec: + state: gathered + +- name: Render configuration without touching the device + vyos.vyos.vyos_vpn_ipsec: + config: + esp_group: + - name: ESP-TEST + proposal: + - proposal_id: 1 + encryption: aes256 + hash: sha256 + state: rendered + +- name: Parse raw config text into structured facts + vyos.vyos.vyos_vpn_ipsec: + running_config: "{{ lookup('file', './vpn_ipsec.cfg') }}" + state: parsed +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + 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) or C(deleted) + type: list + sample: + - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256 + - set vpn ipsec ike-group IKE-TEST key-exchange ikev2 +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 vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict + 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: dict + 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.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsec, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vpn_ipsecArgs.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 = Vpn_ipsec(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_vpn_ipsec_s2s.py b/plugins/modules/vyos_vpn_ipsec_s2s.py new file mode 100644 index 00000000..7458381e --- /dev/null +++ b/plugins/modules/vyos_vpn_ipsec_s2s.py @@ -0,0 +1,337 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2026 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_vpn_ipsec_s2s +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: vyos_vpn_ipsec_s2s +short_description: Manages IPsec site-to-site VPN peers on VyOS network devices. +description: This module manages VPN IPsec site-to-site peer configuration on VyOS + devices -- policy-based tunnels and route-based (VTI) connections. IKE/ESP groups, + PSK/PPK authentication, and IPsec profiles are managed by the separate vyos_vpn_ipsec + module; peers here reference those by name. +version_added: 1.0.0 +author: Evgeny Molotkov (@omnom62) +extends_documentation_fragment: + - vyos.vyos.vyos +notes: + - Tested against VyOS 1.4 and 1.5. + - "Source of truth: vyos-1x's interface-definitions/vpn_ipsec.xml.in, resolved and + drafted via this collection's fetch_vyos_xml_definition.py / parse_xml_definitions.py + helper scripts, then hand-reviewed." + - "The argspec only requires I(name) on a peer, but VyOS itself enforces + several more requirements at commit time -- confirmed via real device + testing, not visible in the argspec: every peer needs C(authentication), + a real C(remote_address) (not just omitted), a C(local_address) or + C(dhcp_interface), and at least one of C(tunnel) or C(vti). A peer + missing any of these will pass Ansible's own argument validation but + fail the device commit with a specific error naming what's missing." +options: + config: + description: IPsec site-to-site configuration. + type: dict + suboptions: + peer: + description: List of site-to-site peers. + type: list + elements: dict + suboptions: + name: + description: Connection name of the peer. + type: str + required: true + disable: + description: Disable this peer. + type: bool + authentication: + description: Peer authentication settings. + type: dict + suboptions: + local_id: + description: Local ID for peer authentication. + type: str + remote_id: + description: ID for remote authentication. + type: str + mode: + description: Authentication mode. + type: str + choices: [pre-shared-secret, rsa, x509] + use_x509_id: + description: Use certificate common name as ID. + type: bool + ppk: + description: Post-quantum preshared key reference for this peer. + type: dict + suboptions: + id: + description: Post-quantum preshared key ID for this connection. + type: str + required: + description: Require a valid PPK for the connection to establish. + type: bool + rsa: + description: RSA key authentication. + type: dict + suboptions: + local_key: + description: Name of the PKI key-pair with the local private key. + type: str + remote_key: + description: Name of the PKI key-pair with the remote public key. + type: str + passphrase: + description: Local private key passphrase. + type: str + x509: + description: X.509 certificate authentication. + type: dict + suboptions: + certificate: + description: Certificate in PKI configuration. + type: str + passphrase: + description: Private key passphrase. + type: str + ca_certificate: + description: Certificate Authority chain in PKI configuration. + type: list + elements: str + childless: + description: Childless IKE SA initiation support. + type: str + choices: [allow, prefer, force, never] + connection_type: + description: Connection type. + type: str + choices: [initiate, trap, none] + default_esp_group: + description: Default ESP group name for tunnels under this peer that + don't specify their own. + type: str + description: + description: Description. + type: str + dhcp_interface: + description: DHCP interface supplying the next-hop IP address. + type: str + force_udp_encapsulation: + description: Force UDP encapsulation. + type: bool + ike_group: + description: IKE group name. + type: str + ikev2_reauth: + description: Re-authentication of the remote peer during an IKE re-key + (IKEv2 only). + type: str + choices: ["yes", "no", inherit] + local_address: + description: IPv4 or IPv6 address of a local interface to use for the + VPN, or "any". + type: str + remote_address: + description: IPv4 or IPv6 address(es) of the remote peer, or "any". + type: list + elements: str + replay_window: + description: IPsec replay window to configure for this CHILD_SA. + type: int + virtual_address: + description: Initiator-requested virtual address(es) from the peer. + type: list + elements: str + tunnel: + description: Policy-based tunnel definitions for this peer. + type: list + elements: dict + suboptions: + tunnel_id: + description: The tunnel identifier. + type: int + required: true + disable: + description: Disable this tunnel. + type: bool + esp_group: + description: ESP group name for this tunnel (overrides the peer's + default_esp_group). + type: str + protocol: + description: Protocol to match for this tunnel's traffic selector. + type: str + priority: + description: Priority for this IPsec policy (lowest value is most + preferred). + type: int + local: + description: Local traffic selector for this tunnel. + type: dict + suboptions: + port: + description: Local port to match. + type: int + prefix: + description: Local IPv4 or IPv6 prefix(es) to match. + type: list + elements: str + remote: + description: Remote traffic selector for this tunnel. + type: dict + suboptions: + port: + description: Remote port to match. + type: int + prefix: + description: Remote IPv4 or IPv6 prefix(es) to match. + type: list + elements: str + vti: + description: Route-based (VTI) connection settings for this peer. + type: dict + suboptions: + bind: + description: VTI tunnel interface associated with this connection. + type: str + esp_group: + description: ESP group name for this VTI connection. + type: str + traffic_selector: + description: Traffic selector for the VTI connection. + type: dict + suboptions: + local: + description: Local traffic-selector parameters. + type: dict + suboptions: + prefix: + description: Local IPv4 or IPv6 prefix(es). + type: list + elements: str + remote: + description: Remote traffic-selector parameters. + type: dict + suboptions: + prefix: + description: Remote IPv4 or IPv6 prefix(es). + type: list + elements: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the VyOS device + by executing the command B(show configuration commands | match "vpn ipsec + site-to-site"). + - The state I(parsed) reads the configuration from the C(running_config) option + and transforms it into Ansible structured data as per the resource module's + argspec, returned in the I(parsed) key within the result. + type: str + state: + description: The state the configuration should be left in. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +""" + +EXAMPLES = """ +- name: Merge a site-to-site peer + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + peer: + - name: PEER-TEST + ike_group: IKE-TEST + default_esp_group: ESP-TEST + remote_address: + - 203.0.113.1 + state: merged +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + 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) or C(deleted) + type: list + sample: + - set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST' + - set vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST' +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 vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST' +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict + 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: dict + 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.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2s, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vpn_ipsec_s2sArgs.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 = Vpn_ipsec_s2s(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_vrf.py b/plugins/modules/vyos_vrf.py new file mode 100644 index 00000000..3f7ae10e --- /dev/null +++ b/plugins/modules/vyos_vrf.py @@ -0,0 +1,1542 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for vyos_vrf +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ +module: vyos_vrf +version_added: 1.0.0 +short_description: VRF resource module +description: +- This module manages vrf configuration on devices running Vyos +author: +- Evgeny Molotkov (@omnom62) +notes: +- Tested against vyos 1.4.2 and 1.5-stream-2025-Q1 +- This module works with connection C(network_cli). +options: + config: + description: List of vrf configuration. + type: dict + suboptions: + bind_to_all: + default: false + description: Enable binding services to all VRFs + type: bool + instances: + description: Virtual Routing and Forwarding instance + type: list + elements: dict + suboptions: + name: + description: VRF instance name + required: true + type: str + description: + description: Description + type: str + disable: + default: false + description: Administratively disable interface + type: bool + aliases: ['disabled'] + table_id: + description: Routing table associated with this instance + type: int + vni: + description: Virtual Network Identifier + type: int + address_family: + type: list + elements: dict + description: Address family configuration + suboptions: + afi: + description: Address family identifier + type: str + choices: ['ipv4', 'ipv6'] + disable_forwarding: + default: false + description: Disable forwarding for this address family + type: bool + nht_no_resolve_via_default: + default: false + description: Disable next-hop resolution via default route + type: bool + route_maps: + description: List of route maps for this address family + type: list + elements: dict + suboptions: + rm_name: + description: Route map name + type: str + required: true + protocol: + description: Protocol to which the route map applies + type: str + choices: + - any + - babel + - bgp + - eigrp + - isis + - ospf + - rip + - static + protocols: + # type: list # sanity + # elements: dict + type: dict + description: Protocol configuration + suboptions: + bgp: + type: dict + description: BGP configuration + suboptions: + as_number: + description: + - AS number. + type: int + #maximum_paths: --> moved to address-family before 1.3 + neighbor: + description: BGP neighbor + type: list + elements: dict + suboptions: + address: + description: + - BGP neighbor address (v4/v6). + type: str + advertisement_interval: + description: + - Minimum interval for sending routing updates. + type: int + capability: + description: + - Advertise capabilities to this neighbor. + type: dict + suboptions: + dynamic: + description: + - Advertise dynamic capability to this neighbor. + type: bool + extended_nexthop: + description: + - Advertise extended nexthop capability to this neighbor. + type: bool + default_originate: + description: + - Send default route to this neighbor + type: str + description: + description: + - Description of the neighbor + type: str + disable_capability_negotiation: + description: + - Disbale capability negotiation with the neighbor + type: bool + disable_connected_check: + description: + - Disable check to see if EBGP peer's address is a connected route. + type: bool + disable_send_community: + description: + - Disable sending community attributes to this neighbor. + type: str + choices: ['extended', 'standard'] + ebgp_multihop: + description: + - Allow this EBGP neighbor to not be on a directly connected network. Specify + the number hops. + type: int + local_as: + description: local as number not to be prepended to updates from EBGP peers + type: int + override_capability: + description: Ignore capability negotiation with specified neighbor. + type: bool + passive: + description: Do not initiate a session with this neighbor + type: bool + password: + description: BGP MD5 password + type: str + peer_group_name: + description: IPv4 peer group for this peer + type: str + peer_group: + description: True if all the configs under this neighbor key is for peer group template. + type: bool + port: + description: Neighbor's BGP port + type: int + remote_as: + description: Neighbor BGP AS number + type: int + shutdown: + description: Administratively shut down neighbor + type: bool + solo: # <-- added in 1.3 + description: Do not send back prefixes learned from the neighbor + type: bool + strict_capability_match: + description: Enable strict capability negotiation + type: bool + timers: + description: Neighbor timers + type: dict + suboptions: + connect: + description: BGP connect timer for this neighbor. + type: int + holdtime: + description: BGP hold timer for this neighbor + type: int + keepalive: + description: BGP keepalive interval for this neighbor + type: int + ttl_security: + description: Number of the maximum number of hops to the BGP peer + type: int + update_source: + description: Source IP of routing updates + type: str + timers: + description: BGP protocol timers + type: dict + suboptions: + keepalive: + description: Keepalive interval + type: int + holdtime: + description: Hold time interval + type: int + bgp_params: + description: BGP parameters + type: dict + suboptions: + always_compare_med: + description: Always compare MEDs from different neighbors + type: bool + bestpath: + description: Default bestpath selection mechanism + type: dict + suboptions: + as_path: + description: AS-path attribute comparison parameters + type: str + choices: ['confed', 'ignore'] + compare_routerid: + description: Compare the router-id for identical EBGP paths + type: bool + med: + description: MED attribute comparison parameters + type: str + choices: ['confed', 'missing-as-worst'] + cluster_id: + description: Route-reflector cluster-id + type: str + confederation: + description: AS confederation parameters + type: list + elements: dict + suboptions: + identifier: + description: Confederation AS identifier + type: int + peers: + description: Peer ASs in the BGP confederation + type: int + dampening: + description: Enable route-flap dampening + type: dict + suboptions: + half_life: + description: Half-life penalty in seconds + type: int + max_suppress_time: + description: Maximum duration to suppress a stable route + type: int + re_use: + description: Time to start reusing a route + type: int + start_suppress_time: + description: When to start suppressing a route + type: int + default: + description: BGP defaults + type: dict + suboptions: + local_pref: + description: Default local preference + type: int + no_ipv4_unicast: + description: | + Deactivate IPv4 unicast for a peer by default + Deprecated: Unavailable after 1.4 + type: bool + deterministic_med: + description: Compare MEDs between different peers in the same AS + type: bool + disable_network_import_check: + description: Disable IGP route check for network statements + type: bool + distance: + description: Administrative distances for BGP routes + type: list + elements: dict + suboptions: + type: + description: Type of route + type: str + choices: ['external', 'internal', 'local'] + value: + description: distance + type: int + prefix: + description: Administrative distance for a specific BGP prefix + type: int + enforce_first_as: + description: Require first AS in the path to match peer's AS + type: bool + graceful_restart: + description: Maximum time to hold onto restarting peer's stale paths + type: int + log_neighbor_changes: + description: Log neighbor up/down changes and reset reason + type: bool + no_client_to_client_reflection: + description: Disable client to client route reflection + type: bool + no_fast_external_failover: + description: Disable immediate session reset if peer's connected link goes down + type: bool + router_id: + description: BGP router-id + type: str + scan_time: + description: BGP route scanner interval + type: int + ospf: + type: dict + description: OSPFv2 configuration + suboptions: + areas: + description: OSPFv2 area. + type: list + elements: dict + suboptions: + area_id: + description: OSPFv2 area identity. + type: str + area_type: + description: Area type. + type: dict + suboptions: + normal: + description: Normal OSPFv2 area. + type: bool + nssa: + description: NSSA OSPFv2 area. + type: dict + suboptions: + set: + description: Enabling NSSA. + type: bool + default_cost: + description: Summary-default cost of NSSA area. + type: int + no_summary: + description: Do not inject inter-area routes into stub. + type: bool + translate: + description: NSSA-ABR. + type: str + choices: [always, candidate, never] + stub: + description: Stub OSPFv2 area. + type: dict + suboptions: + set: + description: Enabling stub. + type: bool + default_cost: + description: Summary-default cost of stub area. + type: int + no_summary: + description: Do not inject inter-area routes into stub. + type: bool + authentication: + description: OSPFv2 area authentication type. + type: str + choices: [plaintext-password, md5] + network: + description: OSPFv2 network. + type: list + elements: dict + suboptions: + address: + required: true + description: OSPFv2 IPv4 network address. + type: str + range: + description: Summarize routes matching prefix (border routers only). + type: list + elements: dict + suboptions: + address: + description: border router IPv4 address. + type: str + cost: + description: Metric for this range. + type: int + not_advertise: + description: Don't advertise this range. + type: bool + substitute: + description: Announce area range (IPv4 address) as another prefix. + type: str + shortcut: + description: Area's shortcut mode. + type: str + choices: [default, disable, enable] + virtual_link: + description: Virtual link address. + type: list + elements: dict + suboptions: + address: + description: virtual link address. + type: str + authentication: + description: OSPFv2 area authentication type. + type: dict + suboptions: + md5: + description: MD5 key id based authentication. + type: list + elements: dict + suboptions: + key_id: + description: MD5 key id. + type: int + md5_key: + description: MD5 key. + type: str + plaintext_password: + description: Plain text password. + type: str + dead_interval: + description: Interval after which a neighbor is declared dead. + type: int + hello_interval: + description: Interval between hello packets. + type: int + retransmit_interval: + description: Interval between retransmitting lost link state advertisements. + type: int + transmit_delay: + description: Link state transmit delay. + type: int + log_adjacency_changes: + description: Log changes in adjacency state. + type: str + choices: [detail] + max_metric: + description: OSPFv2 maximum/infinite-distance metric. + type: dict + suboptions: + router_lsa: + description: Advertise own Router-LSA with infinite distance (stub router). + type: dict + suboptions: + administrative: + description: Administratively apply, for an indefinite period. + type: bool + on_shutdown: + description: Time to advertise self as stub-router. + type: int + on_startup: + description: Time to advertise self as stub-router + type: int + auto_cost: + description: Calculate OSPFv2 interface cost according to bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: Reference bandwidth cost in Mbits/sec. + type: int + default_information: + description: Control distribution of default information. + type: dict + suboptions: + originate: + description: Distribute a default route. + type: dict + suboptions: + always: + description: Always advertise default route. + type: bool + metric: + description: OSPFv2 default metric. + type: int + metric_type: + description: OSPFv2 Metric types for default routes. + type: int + route_map: + description: Route map references. + type: str + default_metric: + description: Metric of redistributed routes + type: int + distance: + description: Administrative distance. + type: dict + suboptions: + global: + description: Global OSPFv2 administrative distance. + type: int + ospf: + description: OSPFv2 administrative distance. + type: dict + suboptions: + external: + description: Distance for external routes. + type: int + inter_area: + description: Distance for inter-area routes. + type: int + intra_area: + description: Distance for intra-area routes. + type: int + mpls_te: + description: MultiProtocol Label Switching-Traffic Engineering (MPLS-TE) parameters. + type: dict + suboptions: + enabled: + description: Enable MPLS-TE functionality. + type: bool + router_address: + description: Stable IP address of the advertising router. + type: str + neighbor: + description: Neighbor IP address. + type: list + elements: dict + suboptions: + neighbor_id: + description: Identity (number/IP address) of neighbor. + type: str + poll_interval: + description: Seconds between dead neighbor polling interval. + type: int + priority: + description: Neighbor priority. + type: int + parameters: + description: OSPFv2 specific parameters. + type: dict + suboptions: + abr_type: + description: OSPFv2 ABR Type. + type: str + choices: [cisco, ibm, shortcut, standard] + opaque_lsa: + description: Enable the Opaque-LSA capability (rfc2370). + type: bool + rfc1583_compatibility: + description: Enable rfc1583 criteria for handling AS external routes. + type: bool + router_id: + description: Override the default router identifier. + type: str + passive_interface: + description: Suppress routing updates on an interface. + type: list + elements: str + passive_interface_exclude: + description: Interface to exclude when using passive-interface default. + type: list + elements: str + redistribute: + description: Redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + route_type: + description: Route type to redistribute. + type: str + choices: [bgp, connected, kernel, rip, static] + metric: + description: Metric for redistribution routes. + type: int + metric_type: + description: OSPFv2 Metric types. + type: int + route_map: + description: Route map references. + type: str + route_map: + description: Filter routes installed in local route map. + type: list + elements: str + timers: + description: Adjust routing timers. + type: dict + suboptions: + refresh: + description: Adjust refresh parameters. + type: dict + suboptions: + timers: + description: refresh timer. + type: int + throttle: + description: Throttling adaptive timers. + type: dict + suboptions: + spf: + description: OSPFv2 SPF timers. + type: dict + suboptions: + delay: + description: Delay (msec) from first change received till SPF + calculation. + type: int + initial_holdtime: + description: Initial hold time(msec) between consecutive SPF calculations. + type: int + max_holdtime: + description: maximum hold time (sec). + type: int + ospfv3: + type: dict + description: OSPFv3 configuration + suboptions: + areas: + description: OSPFv3 area. + type: list + elements: dict + suboptions: + area_id: + description: OSPFv3 Area name/identity. + type: str + export_list: + description: Name of export-list. + type: str + import_list: + description: Name of import-list. + type: str + interface: + description: Enable OSPVv3 on an interface for this area. + aliases: ['interfaces'] + type: list + elements: dict + suboptions: + name: + description: Interface name. + type: str + range: + description: Summarize routes matching prefix (border routers only). + type: list + elements: dict + suboptions: + address: + description: border router IPv4 address. + type: str + advertise: + description: Advertise this range. + type: bool + not_advertise: + description: Don't advertise this range. + type: bool + parameters: + description: OSPFv3 specific parameters. + type: dict + suboptions: + router_id: + description: Override the default router identifier. + type: str + redistribute: + description: Redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + route_type: + description: Route type to redistribute. + type: str + choices: + - bgp + - connected + - kernel + - ripng + - static + route_map: + description: Route map references. + type: str + static: + type: list + description: Static routes configuration + elements: dict + suboptions: + address_families: + description: A dictionary specifying the address family to which the static + route(s) belong. + type: list + elements: dict + suboptions: + afi: + description: + - Specifies the type of route. + type: str + choices: + - ipv4 + - ipv6 + required: true + routes: + description: A dictionary that specify the static route configurations. + type: list + elements: dict + suboptions: + dest: + description: + - An IPv4/v6 address in CIDR notation that specifies the destination + network for the static route. + type: str + required: true + blackhole_config: + description: + - Configured to silently discard packets. + type: dict + suboptions: + type: + description: + - This is to configure only blackhole. + type: str + distance: + description: + - Distance for the route. + type: int + next_hops: + description: + - Next hops to the specified destination. + type: list + elements: dict + suboptions: + forward_router_address: + description: + - The IP address of the next hop that can be used to reach the + destination network. + type: str + enabled: + description: + - Disable IPv4/v6 next-hop static route. + type: bool + admin_distance: + description: + - Distance value for the route. + type: int + interface: + description: + - Name of the outgoing interface. + type: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the VYOS device by + executing the command B(show configuration commands | match "set vrf"). + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - The state I(parsed) reads the configuration from C(show configuration commands | match "set vrf") option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + state: + description: + - The state the configuration should be left in. + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged +""" + +EXAMPLES = """ +# # ------------------- +# # 1. Using merged +# # ------------------- + +# # Before state: +# # ------------- +# vyos@vyos:~$ show configuration commands | match 'set vrf' +# set vrf name vrf-blue description 'blue-vrf' +# set vrf name vrf-blue disable +# set vrf name vrf-blue table '100' +# set vrf name vrf-blue vni '1000' +# vyos@vyos:~$ + +# # Task +# # ------------- + # - name: Merge provided configuration with device configuration + # vyos.vyos.vyos_vrf: + # config: + # instances: + # - name: "vrf-green" + # description: "green-vrf" + # table_id: 110 + # vni: 1010 + +# Task output: +# ------------- + # "after": { + # "bind_to_all": false, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": true, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "description": "green-vrf", + # "disable": false, + # "name": "vrf-green", + # "table_id": 110, + # "vni": 1010 + # } + # ] + # }, + # "before": { + # "bind_to_all": false, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": true, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # } + # ] + # }, + # "changed": true, + # "commands": [ + # "set vrf name vrf-green table 110", + # "set vrf name vrf-green vni 1010", + # "set vrf name vrf-green description green-vrf" + # ] + +# After state: +# # ------------- +# vyos@vyos:~$ show configuration commands | match 'set vrf' +# set vrf name vrf-blue description 'blue-vrf' +# set vrf name vrf-blue disable +# set vrf name vrf-blue table '100' +# set vrf name vrf-blue vni '1000' +# set vrf name vrf-green description 'green-vrf' +# set vrf name vrf-green table '110' +# set vrf name vrf-green vni '1010' +# vyos@vyos:~$ + +# # ------------------- +# # 2. Using replaced +# # ------------------- + +# # Before state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + + +# # Task +# # ------------- + # - name: Merge provided configuration with device configuration + # vyos.vyos.vyos_vrf: + # config: + # bind_to_all: true + # instances: + # - name: "vrf-blue" + # description: "blue-vrf" + # disable: false + # table_id: 100 + # vni: 1002 + # - name: "vrf-red" + # description: "red-vrf" + # disable: false + # table_id: 101 + # vni: 1001 + # address_family: + # - afi: "ipv4" + # disable_forwarding: false + # route_maps: + # - rm_name: "rm1" + # protocol: "ospf" + # - afi: "ipv6" + # nht_no_resolve_via_default: true + # state: replaced + +# # Task output: +# # ------------- + # "after": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": false, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1002 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": false, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "ospf", + # "rm_name": "rm1" + # }, + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # }, + # { + # "afi": "ipv6", + # "disable_forwarding": false, + # "nht_no_resolve_via_default": true + # } + # ], + # "description": "red-vrf", + # "disable": false, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "before": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": false, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": true, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "changed": true, + # "commands": [ + # "set vrf name vrf-blue vni 1002", + # "delete vrf name vrf-red disable", + # "set vrf name vrf-red ip protocol ospf route-map rm1", + # "delete vrf name vrf-red ip disable-forwarding", + # "set vrf name vrf-red ipv6 nht no-resolve-via-default" + # ] + +# After state: +# # ------------- + # vyos@vyos:~$ + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1002' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red ip protocol ospf route-map 'rm1' + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red ipv6 nht no-resolve-via-default + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + + +# # ------------------- +# # 3. Using overridden +# # ------------------- + +# # Before state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# Task +# ------------- + # - name: Overridden provided configuration with device configuration + # vyos.vyos.vyos_vrf: + # config: + # bind_to_all: true + # instances: + # - name: "vrf-blue" + # description: "blue-vrf" + # disable: true + # table_id: 100 + # vni: 1000 + # - name: "vrf-red" + # description: "red-vrf" + # disable: true + # table_id: 101 + # vni: 1001 + # address_family: + # - afi: "ipv4" + # disable_forwarding: false + # route_maps: + # - rm_name: "rm1" + # protocol: "rip" + # - afi: "ipv6" + # nht_no_resolve_via_default: false + # state: overridden + +# # Task output: +# # ------------- + # "after": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": true, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": false, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "before": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": false, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": true, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "changed": true, + # "commands": [ + # "delete vrf name vrf-blue", + # "commit", + # "delete vrf name vrf-red", + # "commit", + # "set vrf name vrf-blue table 100", + # "set vrf name vrf-blue vni 1000", + # "set vrf name vrf-blue description blue-vrf", + # "set vrf name vrf-blue disable", + # "set vrf name vrf-red table 101", + # "set vrf name vrf-red vni 1001", + # "set vrf name vrf-red description red-vrf", + # "set vrf name vrf-red disable", + # "set vrf name vrf-red ip protocol rip route-map rm1" + # ] + +# After state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue disable + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# 4. Using gathered +# ------------------- + +# # Before state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# Task +# ------------- +# - name: Gather provided configuration with device configuration +# vyos.vyos.vyos_vrf: +# config: +# state: gathered + +# # Task output: +# # ------------- + # "gathered": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": false, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": true, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # } + +# After state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + + +# # ------------------- +# # 5. Using deleted +# # ------------------- + +# # Before state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf bind-to-all + # set vrf name vrf-blue description 'blue-vrf' + # set vrf name vrf-blue table '100' + # set vrf name vrf-blue vni '1000' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# # Task +# # ------------- +# - name: Replace provided configuration with device configuration +# vyos.vyos.vyos_vrf: +# config: +# bind_to_all: false +# instances: +# - name: "vrf-blue" +# state: deleted + + +# # Task output: +# # ------------- + # "after": { + # "bind_to_all": false, + # "instances": [ + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": true, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "before": { + # "bind_to_all": true, + # "instances": [ + # { + # "description": "blue-vrf", + # "disable": false, + # "name": "vrf-blue", + # "table_id": 100, + # "vni": 1000 + # }, + # { + # "address_family": [ + # { + # "afi": "ipv4", + # "disable_forwarding": true, + # "nht_no_resolve_via_default": false, + # "route_maps": [ + # { + # "protocol": "rip", + # "rm_name": "rm1" + # } + # ] + # } + # ], + # "description": "red-vrf", + # "disable": true, + # "name": "vrf-red", + # "table_id": 101, + # "vni": 1001 + # } + # ] + # }, + # "changed": true, + # "commands": [ + # "delete vrf bind-to-all", + # "delete vrf name vrf-blue" + # ] + +# After state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# # ------------------- +# # 6. Using rendered +# # ------------------- + +# # Before state: +# # ------------- + # vyos@vyos:~$ show configuration commands | match 'set vrf' + # set vrf name vrf-red description 'red-vrf' + # set vrf name vrf-red disable + # set vrf name vrf-red ip disable-forwarding + # set vrf name vrf-red ip protocol rip route-map 'rm1' + # set vrf name vrf-red table '101' + # set vrf name vrf-red vni '1001' + # vyos@vyos:~$ + +# Task +# ------------- + # - name: Render provided configuration with device configuration + # vyos.vyos.vyos_vrf: + # config: + # bind_to_all: true + # instances: + # - name: "vrf-green" + # description: "green-vrf" + # disabled: true + # table_id: 105 + # vni: 1000 + # - name: "vrf-amber" + # description: "amber-vrf" + # disable: false + # table_id: 111 + # vni: 1001 + # address_family: + # - afi: "ipv4" + # disable_forwarding: true + # route_maps: + # - rm_name: "rm1" + # protocol: "ospf" + # - afi: "ipv6" + # nht_no_resolve_via_default: false + # state: rendered + +# # Task output: +# # ------------- + # "rendered": [ + # "set vrf bind-to-all", + # "set vrf name vrf-green table 105", + # "set vrf name vrf-green vni 1000", + # "set vrf name vrf-green description green-vrf", + # "set vrf name vrf-green disable", + # "set vrf name vrf-amber table 111", + # "set vrf name vrf-amber vni 1001", + # "set vrf name vrf-amber description amber-vrf", + # "set vrf name vrf-amber ip protocol ospf route-map rm1", + # "set vrf name vrf-amber ip disable-forwarding" + # ] + +# # ------------------- +# # 7. Using parsed +# # ------------------- + +# # vrf_parsed.cfg: +# # ------------- +# set vrf bind-to-all +# set vrf name vrf1 description 'red' +# set vrf name vrf1 disable +# set vrf name vrf1 table 101 +# set vrf name vrf1 vni 501 +# set vrf name vrf2 description 'blah2' +# set vrf name vrf2 disable +# set vrf name vrf2 table 102 +# set vrf name vrf2 vni 102 +# set vrf name vrf1 ip disable-forwarding +# set vrf name vrf1 ip nht no-resolve-via-default +# set vrf name vrf-red ip protocol ospf route-map 'rm1' +# set vrf name vrf-red ipv6 nht no-resolve-via-default + +# Task: +# ------------- +# - name: Parse provided configuration with device configuration +# vyos.vyos.vyos_vrf: +# running_config: "{{ lookup('file', './vrf_parsed.cfg') }}" +# state: parsed + + +# # Task output: +# # ------------- +# "parsed": { +# "bind_to_all": true, +# "instances": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "disable_forwarding": true, +# "nht_no_resolve_via_default": true +# } +# ], +# "description": "red", +# "disable": true, +# "name": "vrf1" +# }, +# { +# "description": "blah2", +# "disable": true, +# "name": "vrf2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "disable_forwarding": false, +# "nht_no_resolve_via_default": false, +# "route_maps": [ +# { +# "protocol": "ospf", +# "rm_name": "rm1" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "disable_forwarding": false, +# "nht_no_resolve_via_default": true +# } +# ], +# "disable": false, +# "name": "vrf-red" +# } +# ] +# } +""" + +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 system ntp server server1 dynamic + - set system ntp server server1 prefer + - set system ntp server server2 noselect + - set system ntp server server2 preempt + - set system ntp server server_add preempt +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 system ntp server server1 dynamic + - set system ntp server server1 prefer + - set system ntp server server2 noselect + - set system ntp server server2 preempt + - set system ntp server server_add preempt +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.vrf.vrf import VrfArgs +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vrf.vrf import Vrf + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=VrfArgs.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 = Vrf(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/plugins/terminal/vyos.py b/plugins/terminal/vyos.py index cbe98939..29acb803 100644 --- a/plugins/terminal/vyos.py +++ b/plugins/terminal/vyos.py @@ -18,7 +18,6 @@ # from __future__ import absolute_import, division, print_function - __metaclass__ = type import os |
