diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-21 22:12:11 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-21 15:12:11 +0300 |
| commit | 899a6bf7955592ec40670944a860a1bee97b432c (patch) | |
| tree | fa35031ba10df9ab41376529363664dda0ea7d4c /plugins/module_utils/network | |
| parent | d80f14eef50403f0851c9a12b77890c9bf1f3c4d (diff) | |
| download | vyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.tar.gz vyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.zip | |
T8321: vpn_ipsec modules (#489)
Add VPN IPsec module
Diffstat (limited to 'plugins/module_utils/network')
15 files changed, 3315 insertions, 0 deletions
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/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/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 2a789103..69cb0d0b 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -83,6 +83,12 @@ 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) @@ -109,6 +115,8 @@ FACT_RESOURCE_SUBSETS = dict( 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/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/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 |
