summaryrefslogtreecommitdiff
path: root/plugins/module_utils/network
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/module_utils/network')
-rw-r--r--plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/facts/facts.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py77
-rw-r--r--plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py4
-rw-r--r--plugins/module_utils/network/vyos/argspec/ha/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/ha/ha.py196
-rw-r--r--plugins/module_utils/network/vyos/argspec/hostname/hostname.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py20
-rw-r--r--plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/nat/nat.py652
-rw-r--r--plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py1
-rw-r--r--plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py2
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py206
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py186
-rw-r--r--plugins/module_utils/network/vyos/argspec/vrf/vrf.py1
-rw-r--r--plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py1
-rw-r--r--plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py331
-rw-r--r--plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py2
-rw-r--r--plugins/module_utils/network/vyos/config/ha/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/ha/ha.py705
-rw-r--r--plugins/module_utils/network/vyos/config/hostname/hostname.py1
-rw-r--r--plugins/module_utils/network/vyos/config/interfaces/interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py40
-rw-r--r--plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py2
-rw-r--r--plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/config/logging_global/logging_global.py68
-rw-r--r--plugins/module_utils/network/vyos/config/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/nat/nat.py577
-rw-r--r--plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py4
-rw-r--r--plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py2
-rw-r--r--plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py1
-rw-r--r--plugins/module_utils/network/vyos/config/route_maps/route_maps.py1
-rw-r--r--plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py1
-rw-r--r--plugins/module_utils/network/vyos/config/static_routes/static_routes.py1
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py665
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py355
-rw-r--r--plugins/module_utils/network/vyos/config/vrf/vrf.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/facts.py19
-rw-r--r--plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py128
-rw-r--r--plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py4
-rw-r--r--plugins/module_utils/network/vyos/facts/ha/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/ha/ha.py174
-rw-r--r--plugins/module_utils/network/vyos/facts/hostname/hostname.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/interfaces/interfaces.py4
-rw-r--r--plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/legacy/base.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/logging_global/logging_global.py21
-rw-r--r--plugins/module_utils/network/vyos/facts/nat/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/nat/nat.py174
-rw-r--r--plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py2
-rw-r--r--plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/route_maps/route_maps.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/static_routes/static_routes.py1
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py113
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py115
-rw-r--r--plugins/module_utils/network/vyos/facts/vrf/vrf.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/bgp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/ha.py1011
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/hostname.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/logging_global.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/logging_global_15.py241
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/nat.py1186
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/ntp_global.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py23
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/prefix_lists.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/route_maps.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/route_maps_14.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/snmp_server.py1
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py997
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py670
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vrf.py1
-rw-r--r--plugins/module_utils/network/vyos/utils/utils.py57
-rw-r--r--plugins/module_utils/network/vyos/vyos.py29
-rw-r--r--plugins/module_utils/network/vyos/vyos_file.py99
112 files changed, 9050 insertions, 188 deletions
diff --git a/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py
index 13f9fab0..946f6861 100644
--- a/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py
+++ b/plugins/module_utils/network/vyos/argspec/bgp_address_family/bgp_address_family.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py
index 42fb5abf..13cca54d 100644
--- a/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py
+++ b/plugins/module_utils/network/vyos/argspec/bgp_global/bgp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/facts/facts.py b/plugins/module_utils/network/vyos/argspec/facts/facts.py
index b274c507..ddcd8d14 100644
--- a/plugins/module_utils/network/vyos/argspec/facts/facts.py
+++ b/plugins/module_utils/network/vyos/argspec/facts/facts.py
@@ -4,8 +4,8 @@
"""
The arg spec for the vyos facts module.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
index 8421b6de..dd17ef85 100644
--- a/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/argspec/firewall_global/firewall_global.py
@@ -4,31 +4,12 @@
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-#############################################
-# WARNING #
-#############################################
-#
-# This file is auto generated by the resource
-# module builder playbook.
-#
-# Do not edit this file manually.
-#
-# Changes to this file will be over written
-# by the resource module builder.
-#
-# Changes should be made in the model used to
-# generate this file or in the resource module
-# builder template.
-#
-#############################################
"""
The arg spec for the vyos_firewall_global module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
@@ -187,6 +168,64 @@ class Firewall_globalArgs(object): # pylint: disable=R0903
],
"type": "str",
},
+ "zone": {
+ "elements": "dict",
+ "type": "list",
+ "options": {
+ "description": {"type": "str"},
+ "default_action": {
+ "choices": [
+ "drop",
+ "reject",
+ ],
+ "default": "drop",
+ "type": "str",
+ },
+ "default_log": {"type": "bool"},
+ "interfaces": {
+ "elements": "str",
+ "type": "list",
+ },
+ "local_zone": {"type": "bool"},
+ "name": {
+ "required": True,
+ "type": "str",
+ },
+ "intra_zone_filtering": {
+ "type": "dict",
+ "options": {
+ "action": {
+ "choices": ["accept", "drop"],
+ "type": "str",
+ },
+ "firewall": {
+ "type": "dict",
+ "options": {
+ "name": {"type": "str"},
+ "ipv6_name": {"type": "str"},
+ },
+ },
+ },
+ },
+ "sources": {
+ "elements": "dict",
+ "type": "list",
+ "options": {
+ "zone": {
+ "required": True,
+ "type": "str",
+ },
+ "firewall": {
+ "type": "dict",
+ "options": {
+ "name": {"type": "str"},
+ "ipv6_name": {"type": "str"},
+ },
+ },
+ },
+ },
+ },
+ },
},
"type": "dict",
},
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py
index 93c898e8..d925a7f4 100644
--- a/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/firewall_interfaces/firewall_interfaces.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_firewall_interfaces module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py
index d1cf4fe8..0378a65b 100644
--- a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py
+++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_firewall_rules module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
@@ -56,6 +54,8 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903
"reject",
"accept",
"jump",
+ "return",
+ "continue",
],
"type": "str",
},
diff --git a/plugins/module_utils/network/vyos/argspec/ha/__init__.py b/plugins/module_utils/network/vyos/argspec/ha/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/ha/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/ha/ha.py b/plugins/module_utils/network/vyos/argspec/ha/ha.py
new file mode 100644
index 00000000..4fe431cd
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/ha/ha.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+# Copyright 2024 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_ha module
+"""
+
+
+class HaArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_ha module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "required": False,
+ "options": {
+ "disable": {"type": "bool", "default": False},
+ "virtual_servers": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "address": {"type": "str"},
+ "algorithm": {"type": "str"},
+ "delay_loop": {"type": "int"},
+ "forward_method": {"type": "str", "choices": ["direct", "nat"]},
+ "fwmark": {"type": "int"},
+ "persistence_timeout": {"type": "int"},
+ "port": {"type": "int"},
+ "protocol": {"type": "str", "choices": ["tcp", "udp"]},
+ "real_server": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "address": {"type": "str", "required": True},
+ "port": {"type": "int"},
+ "connection_timeout": {"type": "int"},
+ "health_check_script": {"type": "str"},
+ },
+ },
+ },
+ },
+ "vrrp": {
+ "type": "dict",
+ "options": {
+ "global_parameters": {
+ "type": "dict",
+ "options": {
+ "garp": {
+ "type": "dict",
+ "options": {
+ "interval": {"type": "int"},
+ "master_delay": {"type": "int"},
+ "master_refresh": {"type": "int"},
+ "master_refresh_repeat": {"type": "int"},
+ "master_repeat": {"type": "int"},
+ },
+ },
+ "startup_delay": {"type": "int"},
+ "version": {"type": "str"},
+ },
+ },
+ "groups": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "address": {"type": "list", "elements": "str"},
+ "advertise_interval": {"type": "int"},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "password": {"type": "str", "no_log": True},
+ "type": {"type": "str"},
+ },
+ },
+ "description": {"type": "str"},
+ "disable": {"type": "bool", "default": False},
+ "excluded_address": {"type": "list", "elements": "str"},
+ "garp": {
+ "type": "dict",
+ "options": {
+ "interval": {"type": "int"},
+ "master_delay": {"type": "int"},
+ "master_refresh": {"type": "int"},
+ "master_refresh_repeat": {"type": "int"},
+ "master_repeat": {"type": "int"},
+ },
+ },
+ "health_check": {
+ "type": "dict",
+ "options": {
+ "failure_count": {"type": "int"},
+ "interval": {"type": "int"},
+ "ping": {"type": "str"},
+ "script": {"type": "str"},
+ },
+ },
+ "hello_source_address": {"type": "str"},
+ "interface": {"type": "str"},
+ "no_preempt": {"type": "bool", "default": False},
+ "peer_address": {"type": "str"},
+ "preempt_delay": {"type": "int"},
+ "priority": {"type": "int"},
+ "rfc3768_compatibility": {"type": "bool", "default": False},
+ "track": {
+ "type": "dict",
+ "options": {
+ "exclude_vrrp_interface": {"type": "bool"},
+ "interface": {"type": "list", "elements": "str"},
+ },
+ },
+ "transition_script": {
+ "type": "dict",
+ "options": {
+ "backup": {"type": "str"},
+ "fault": {"type": "str"},
+ "master": {"type": "str"},
+ "stop": {"type": "str"},
+ },
+ },
+ "vrid": {"type": "int", "required": False},
+ },
+ },
+ "snmp": {
+ "type": "str",
+ "choices": ["disabled", "enabled"],
+ },
+ "sync_groups": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "health_check": {
+ "type": "dict",
+ "options": {
+ "failure_count": {"type": "int"},
+ "interval": {"type": "int"},
+ "ping": {"type": "str"},
+ "script": {"type": "str"},
+ },
+ },
+ "member": {"type": "list", "elements": "str"},
+ "transition_script": {
+ "type": "dict",
+ "options": {
+ "backup": {"type": "str"},
+ "fault": {"type": "str"},
+ "master": {"type": "str"},
+ "stop": {"type": "str"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "state": {
+ "type": "str",
+ "choices": [
+ "deleted",
+ "merged",
+ "purged",
+ "replaced",
+ "gathered",
+ "rendered",
+ "parsed",
+ "overridden",
+ ],
+ "default": "merged",
+ },
+ "running_config": {"type": "str"},
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/hostname/hostname.py b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py
index 12864e66..b1af7b9c 100644
--- a/plugins/module_utils/network/vyos/argspec/hostname/hostname.py
+++ b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py
index 14b67c28..8ba119f3 100644
--- a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py
@@ -4,31 +4,12 @@
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-#############################################
-# WARNING #
-#############################################
-#
-# This file is auto generated by the resource
-# module builder playbook.
-#
-# Do not edit this file manually.
-#
-# Changes to this file will be over written
-# by the resource module builder.
-#
-# Changes should be made in the model used to
-# generate this file or in the resource module
-# builder template.
-#
-#############################################
"""
The arg spec for the vyos_interfaces module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
@@ -79,6 +60,7 @@ class InterfacesArgs(object): # pylint: disable=R0903
},
"type": "list",
},
+ "vrf": {"type": "str"},
},
"type": "list",
},
diff --git a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py
index 4dee518e..d6d9e298 100644
--- a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_l3_interfaces module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py
index 956c4385..7744eea0 100644
--- a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_lag_interfaces module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py
index d79de617..6d1129e8 100644
--- a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py
+++ b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_lldp_global module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py
index fd6c6271..4bb742f5 100644
--- a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_lldp_interfaces module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py
index 734d190e..f6800e9b 100644
--- a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py
+++ b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/nat/__init__.py b/plugins/module_utils/network/vyos/argspec/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py
new file mode 100644
index 00000000..6f81c53c
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py
@@ -0,0 +1,652 @@
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The arg spec for the vyos_nat module
+"""
+
+
+class NatArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_nat module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "nat": {
+ "type": "dict",
+ "options": {
+ "cgnat": {
+ "type": "dict",
+ "options": {
+ "log_allocation": {
+ "type": "bool",
+ },
+ "pool": {
+ "type": "dict",
+ "options": {
+ "external": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ "required": True,
+ },
+ "external_port_range": {
+ "type": "str",
+ },
+ "per_user_limit": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "range": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "value": {
+ "type": "str",
+ "required": True,
+ },
+ "seq": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ "internal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ "required": True,
+ },
+ "range": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "packet_type": {
+ "type": "str",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "inbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ "group": {
+ "type": "str",
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "fqdn": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "address_group": {
+ "type": "str",
+ },
+ "domain_group": {
+ "type": "str",
+ },
+ "mac_group": {
+ "type": "str",
+ },
+ "network_group": {
+ "type": "str",
+ },
+ "port_group": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "redirect_port": {
+ "type": "str",
+ },
+ "address_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "persistent",
+ ],
+ },
+ "port_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "none",
+ ],
+ },
+ },
+ },
+ "load_balance": {
+ "type": "dict",
+ "options": {
+ "backend": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "ip": {"type": "str"},
+ "weight": {"type": "int"},
+ },
+ },
+ "hash": {
+ "type": "list",
+ "elements": "str",
+ "choices": [
+ "source-address",
+ "destination-address",
+ "source-port",
+ "destination-port",
+ "random",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "packet_type": {
+ "type": "str",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "outbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ "group": {
+ "type": "str",
+ },
+ },
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "fqdn": {
+ "type": "str",
+ },
+ "address_group": {
+ "type": "str",
+ },
+ "domain_group": {
+ "type": "str",
+ },
+ "mac_group": {
+ "type": "str",
+ },
+ "network_group": {
+ "type": "str",
+ },
+ "port_group": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "address": {"type": "str"},
+ "fqdn": {"type": "str"},
+ "port": {"type": "str"},
+ "address_group": {"type": "str"},
+ "domain_group": {"type": "str"},
+ "mac_group": {"type": "str"},
+ "network_group": {"type": "str"},
+ "port_group": {"type": "str"},
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ "address_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "persistent",
+ ],
+ },
+ "port_mapping": {
+ "type": "str",
+ "choices": [
+ "random",
+ "none",
+ ],
+ },
+ },
+ },
+ "load_balance": {
+ "type": "dict",
+ "options": {
+ "backend": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "ip": {"type": "str"},
+ "weight": {"type": "int"},
+ },
+ },
+ "hash": {
+ "type": "list",
+ "elements": "str",
+ "choices": [
+ "source-address",
+ "destination-address",
+ "source-port",
+ "destination-port",
+ "random",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "static": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ },
+ },
+ "inbound_interface": {
+ "type": "str",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "nat64": {
+ "type": "dict",
+ "options": {
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "match": {
+ "type": "dict",
+ "options": {
+ "mark": {
+ "type": "int",
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "pool": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "address": {
+ "type": "str",
+ },
+ "description": {
+ "type": "str",
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "port": {
+ "type": "str",
+ },
+ "protocol": {
+ "type": "str",
+ "choices": [
+ "icmp",
+ "tcp",
+ "udp",
+ ],
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "nat66": {
+ "type": "dict",
+ "options": {
+ "destination": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "inbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ },
+ },
+ "log": {
+ "type": "bool",
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "rule": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "id": {
+ "type": "int",
+ "required": True,
+ },
+ "description": {
+ "type": "str",
+ },
+ "destination": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "disable": {
+ "type": "bool",
+ },
+ "exclude": {
+ "type": "bool",
+ },
+ "log": {
+ "type": "bool",
+ },
+ "outbound_interface": {
+ "type": "dict",
+ "options": {
+ "name": {
+ "type": "str",
+ },
+ },
+ },
+ "protocol": {
+ "type": "str",
+ },
+ "source": {
+ "type": "dict",
+ "options": {
+ "port": {
+ "type": "str",
+ },
+ "prefix": {
+ "type": "str",
+ },
+ },
+ },
+ "translation": {
+ "type": "dict",
+ "options": {
+ "address": {
+ "type": "str",
+ },
+ "port": {
+ "type": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "deleted",
+ "merged",
+ "overridden",
+ "replaced",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py
index 6940fb7e..5e10d05b 100644
--- a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py
+++ b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py
index 0b5814be..ced9f98e 100644
--- a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py
+++ b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py
index 0a422e89..a939652b 100644
--- a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py
+++ b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_ospfv2 module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py
index 61704345..77a17d79 100644
--- a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py
+++ b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_ospfv3 module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py
index b01a3e82..c74400ba 100644
--- a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py
+++ b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py
index 5b6d404d..58ffa3e5 100644
--- a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py
+++ b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py
index b94c2639..53516c4f 100644
--- a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py
+++ b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py
index 365df48a..d8a4f11a 100644
--- a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py
+++ b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py
@@ -25,10 +25,8 @@
The arg spec for the vyos_static_routes module
"""
-
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..75fc1ce5
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec module
+"""
+
+
+class Vpn_ipsecArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "ike_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "close_action": {
+ "type": "str",
+ "choices": ["none", "trap", "start"],
+ },
+ "dead_peer_detection": {
+ "type": "dict",
+ "options": {
+ "action": {
+ "type": "str",
+ "choices": ["trap", "clear", "restart"],
+ },
+ "interval": {"type": "int"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "disable_mobike": {"type": "bool"},
+ "ikev2_reauth": {"type": "bool"},
+ "key_exchange": {
+ "type": "str",
+ "choices": ["ikev1", "ikev2"],
+ },
+ "lifetime": {"type": "int"},
+ "mode": {"type": "str", "choices": ["main", "aggressive"]},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "dh_group": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ "prf": {"type": "str"},
+ },
+ },
+ },
+ },
+ "esp_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "compression": {"type": "bool"},
+ "disable_rekey": {"type": "bool"},
+ "life_bytes": {"type": "int"},
+ "life_packets": {"type": "int"},
+ "lifetime": {"type": "int"},
+ "mode": {
+ "type": "str",
+ "choices": ["tunnel", "transport"],
+ },
+ "pfs": {"type": "str"},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ },
+ },
+ },
+ },
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "psk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "dhcp_interface": {
+ "type": "list",
+ "elements": "str",
+ },
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ "ppk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ },
+ },
+ "profile": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "mode": {
+ "type": "str",
+ "choices": ["pre-shared-secret"],
+ },
+ "pre_shared_secret": {"type": "str", "no_log": True},
+ },
+ },
+ "bind_tunnel": {"type": "list", "elements": "str"},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "ike_group": {"type": "str"},
+ },
+ },
+ "interface": {"type": "list", "elements": "str"},
+ "log": {
+ "type": "dict",
+ "options": {
+ "level": {"type": "int"},
+ "subsystem": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "options": {
+ "type": "dict",
+ "options": {
+ "disable_route_autoinstall": {"type": "bool"},
+ "flexvpn": {"type": "bool"},
+ "interface": {"type": "str"},
+ "retransmission": {
+ "type": "dict",
+ "options": {
+ "attempts": {"type": "int"},
+ "base": {"type": "float"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "virtual_ip": {"type": "bool"},
+ },
+ },
+ "disable_uniqreqids": {"type": "bool"},
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..bd23d4d3
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec_s2s module
+"""
+
+
+class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec_s2s module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "peer": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "disable": {"type": "bool"},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "local_id": {"type": "str"},
+ "ppk": {
+ "type": "dict",
+ "options": {
+ "id": {"type": "str"},
+ "required": {"type": "bool"},
+ },
+ },
+ "rsa": {
+ "type": "dict",
+ "options": {
+ "local_key": {"type": "str", "no_log": True},
+ "passphrase": {"type": "str", "no_log": True},
+ "remote_key": {"type": "str", "no_log": True},
+ },
+ },
+ "x509": {
+ "type": "dict",
+ "options": {
+ "certificate": {"type": "str"},
+ "passphrase": {"type": "str", "no_log": True},
+ "ca_certificate": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "mode": {
+ "type": "str",
+ "choices": [
+ "pre-shared-secret",
+ "rsa",
+ "x509",
+ ],
+ },
+ "remote_id": {"type": "str"},
+ "use_x509_id": {"type": "bool"},
+ },
+ },
+ "childless": {
+ "type": "str",
+ "choices": ["allow", "prefer", "force", "never"],
+ },
+ "connection_type": {
+ "type": "str",
+ "choices": ["initiate", "trap", "none"],
+ },
+ "default_esp_group": {"type": "str"},
+ "description": {"type": "str"},
+ "dhcp_interface": {"type": "str"},
+ "force_udp_encapsulation": {"type": "bool"},
+ "ike_group": {"type": "str"},
+ "ikev2_reauth": {
+ "type": "str",
+ "choices": ["yes", "no", "inherit"],
+ },
+ "local_address": {"type": "str"},
+ "remote_address": {"type": "list", "elements": "str"},
+ "replay_window": {"type": "int"},
+ "tunnel": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "tunnel_id": {"type": "int", "required": True},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "local": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "protocol": {"type": "str"},
+ "priority": {"type": "int"},
+ "remote": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ "virtual_address": {"type": "list", "elements": "str"},
+ "vti": {
+ "type": "dict",
+ "options": {
+ "bind": {"type": "str"},
+ "esp_group": {"type": "str"},
+ "traffic_selector": {
+ "type": "dict",
+ "options": {
+ "local": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "remote": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/vrf/vrf.py b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py
index e24fd537..20947f9d 100644
--- a/plugins/module_utils/network/vyos/argspec/vrf/vrf.py
+++ b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
#############################################
diff --git a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py
index 2d6481c2..f1494698 100644
--- a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py
+++ b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py
index 075c8fd2..2d7d3e63 100644
--- a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py
+++ b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
index d4e173c1..289037ef 100644
--- a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py
@@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -241,6 +241,8 @@ class Firewall_global(ConfigBase):
commands.extend(self._render_state_policy(key, w, h, opr=opr))
elif key == "route_redirects":
commands.extend(self._render_route_redirects(key, w, h, opr=opr))
+ elif key == "zone":
+ commands.extend(self._render_zone(key, w, h, opr=opr))
return commands
def _add_global_attr(self, w, h, opr=True):
@@ -384,7 +386,9 @@ class Firewall_global(ConfigBase):
)
elif not opr and key in l_set:
if key == "name" and self._is_grp_del(h, want, key):
- if len(commands) > 0 and commands[-1] == cmd + " " + want["name"] + " " + self._grp_type(
+ if len(commands) > 0 and commands[-1] == cmd + " " + want[
+ "name"
+ ] + " " + self._grp_type(
attr,
):
commands.pop()
@@ -690,7 +694,7 @@ class Firewall_global(ConfigBase):
cmd = "set firewall "
if (
attr
- and key != "group"
+ and key not in ["group", "zone"]
and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")
):
cmd += "global-options "
@@ -808,3 +812,324 @@ class Firewall_global(ConfigBase):
elif attrib == "validation":
regex = "source-validation"
return regex
+
+ def _render_zone(self, attr, w, h, opr):
+ """
+ This function forms the commands for group list/members attributes based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param opr: True/False.
+ :return: generated list of commands.
+ """
+ commands = []
+ h_grp = []
+ w_grp = []
+ l_set = ("name", "description", "default_action", "default_log", "local_zone")
+ if w:
+ w_grp = w.get(attr) or []
+ if h:
+ h_grp = h.get(attr) or []
+
+ if w_grp:
+ for want in w_grp:
+ h = self.search_attrib_in_have(h_grp, want, "name")
+
+ cmd = self._compute_command(key="zone", attr="", opr=opr)
+
+ if not opr and self._is_grp_del(h, want, "name"):
+ commands.append(cmd + " " + want["name"])
+ continue
+
+ for key, val in want.items():
+ if val:
+ if opr and key in l_set and not (h and self._is_w_same(want, h, key)):
+ if key == "name":
+ pass
+ elif isinstance(val, bool):
+ commands.append(
+ cmd + " " + want["name"] + " " + key.replace("_", "-"),
+ )
+ else:
+ commands.append(
+ cmd
+ + " "
+ + want["name"]
+ + " "
+ + key.replace("_", "-")
+ + " '"
+ + str(want[key])
+ + "'",
+ )
+ elif not opr and key in l_set:
+ if not (h and in_target_not_none(h, key)) and not self._is_grp_del(
+ h,
+ want,
+ "name",
+ ):
+ commands.append(
+ cmd + " " + want["name"] + " " + key.replace("_", "-"),
+ )
+ elif key == "interfaces":
+ commands.extend(
+ self._render_interfaces(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ elif key == "intra_zone_filtering":
+ commands.extend(
+ self._render_izf(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ elif key == "sources":
+ commands.extend(
+ self._render_sources(
+ key,
+ want,
+ h,
+ opr,
+ cmd,
+ want["name"],
+ attr,
+ ),
+ )
+ return commands
+
+ def _render_interfaces(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for interfaces
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ # VyOS 1.5.0 GA moved 'interface' under a new 'member' node
+ # ("set firewall zone <name> member interface <ifname>"). 1.4.x and
+ # 1.5-rolling snapshots predating this change still use the bare
+ # 'interface' node. Known limitation: a 1.5-rolling build reporting
+ # "1.5" that predates this change will incorrectly get the new
+ # syntax -- accepted trade-off, see PR notes.
+ if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"):
+ iface_kw = "member interface"
+ else:
+ iface_kw = "interface"
+
+ if want:
+ if opr:
+ interfaces = list_diff_want_only(want, have)
+
+ for interface in interfaces:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ elif not opr and have:
+ interfaces = list_diff_want_only(want, have)
+ for interface in interfaces:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ elif not opr and not have:
+ for interface in want:
+ commands.append(
+ cmd + " " + name + " " + iface_kw + " " + interface,
+ )
+ else:
+ self._module.fail_json(msg={"want": want, "have": have, "opr": opr})
+
+ return commands
+
+ def _render_izf(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for intra zone filtering
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ if want:
+ if opr:
+ izfs = self._dict_diff(want, have)
+ for izf in izfs:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " intra-zone-filtering "
+ + izf[0].replace(".", " ")
+ + " "
+ + izf[1],
+ )
+ elif not opr and have:
+ izfs = self._dict_diff(want, have)
+
+ for izf in izfs:
+ commands.append(
+ cmd + " " + name + " intra-zone-filtering " + izf[0].replace(".", " "),
+ )
+ elif not opr and not have:
+ commands.append(
+ cmd + " " + name + " intra-zone-filtering",
+ )
+ return commands
+
+ def _dict_diff(self, want, have, path=""):
+ """
+ Recursively find keys/values in `want` that differ or are missing in `have`.
+ Returns list of tuples: (full_path, value_in_want)
+ """
+ diffs = []
+
+ have = have or {}
+
+ for key, want_val in want.items():
+ current_path = f"{path}.{key.replace('_', '-')}" if path else key.replace("_", "-")
+
+ if key not in have:
+ if isinstance(want_val, dict):
+ diffs.extend(self._dict_diff(want_val, {}, current_path))
+ elif isinstance(want_val, list):
+ for i, item in enumerate(want_val):
+ if isinstance(item, dict):
+ diffs.extend(self._dict_diff(item, {}, f"{current_path}[{i}]"))
+ else:
+ diffs.append((f"{current_path}[{i}]", item))
+ else:
+ diffs.append((current_path, want_val))
+
+ else:
+ have_val = have[key]
+
+ if isinstance(want_val, dict) and isinstance(have_val, dict):
+ diffs.extend(self._dict_diff(want_val, have_val, current_path))
+
+ elif isinstance(want_val, list) and isinstance(have_val, list):
+ for i, item in enumerate(want_val):
+ if i >= len(have_val):
+ diffs.append((f"{current_path}[{i}]", item))
+ elif isinstance(item, dict) and isinstance(have_val[i], dict):
+ diffs.extend(
+ self._dict_diff(item, have_val[i], f"{current_path}[{i}]"),
+ )
+ elif item != have_val[i]:
+ diffs.append((f"{current_path}[{i}]", item))
+
+ elif want_val != have_val:
+ diffs.append((current_path, want_val))
+
+ return diffs
+
+ def _render_sources(self, attr, w, h, opr, cmd, name, type):
+ """
+ This function forms the commands for sources (from)
+ based on the 'opr'.
+ :param attr: attribute name.
+ :param w: the desired config.
+ :param h: the target config.
+ :param cmd: commands to be prepend.
+ :param name: name of group.
+ :param type: group type.
+ :return: generated list of commands.
+ """
+ commands = []
+ have = []
+ if w:
+ want = w.get(attr) or []
+ if h:
+ have = h.get(attr) or []
+
+ have_index = {item["zone"]: item for item in have}
+
+ for item1 in want:
+ zone = item1["zone"]
+
+ if zone in have_index:
+ item2 = have_index[zone]
+
+ wfw = item1.get("firewall", {})
+ hfw = item2.get("firewall", {})
+ if wfw:
+ if opr:
+ sources = self._dict_diff(wfw, hfw)
+ for source in sources:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + source[0].replace("_", "-")
+ + " "
+ + source[1],
+ )
+ elif not opr and hfw:
+ sources = self._dict_diff(wfw, hfw)
+ for source in sources:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + source[0].replace("_", "-"),
+ )
+ elif not opr and not hfw:
+ commands.append(
+ cmd + " " + name + " from " + zone,
+ )
+ elif opr:
+ wfw = item1.get("firewall", {})
+ for key, val in wfw.items():
+ if val:
+ commands.append(
+ cmd
+ + " "
+ + name
+ + " from "
+ + zone
+ + " firewall "
+ + key.replace("_", "-")
+ + " "
+ + val,
+ )
+ elif not opr:
+ commands.append(
+ cmd + " " + name + " from " + zone,
+ )
+ return commands
diff --git a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py
index 85a8042f..ec1aaef1 100644
--- a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py
+++ b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py
@@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py
index 3c768e32..be36d3fe 100644
--- a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py
+++ b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py
@@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/config/ha/__init__.py b/plugins/module_utils/network/vyos/config/ha/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/ha/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/ha/ha.py b/plugins/module_utils/network/vyos/config/ha/ha.py
new file mode 100644
index 00000000..7fc96a35
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/ha/ha.py
@@ -0,0 +1,705 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_ha config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ remove_empties,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import (
+ HaTemplate,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import (
+ LooseVersion,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version
+
+
+class Ha(ResourceModule):
+ """
+ The vyos_ha config class
+ """
+
+ def __init__(self, module):
+ super(Ha, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="ha",
+ tmplt=HaTemplate(),
+ )
+ self.parsers = [
+ "disable",
+ ]
+
+ self._validate_template()
+
+ def _validate_template(self):
+ version = get_os_version(self._module)
+ if LooseVersion(version) >= LooseVersion("1.4"):
+ self._tmplt = HaTemplate()
+ else:
+ self._module.fail_json(msg="High Availability is not supported in this version of VyOS")
+
+ def execute_module(self):
+ """Execute the module
+
+ :rtype: A dictionary
+ :returns: The result from module execution
+ """
+
+ if self.state not in ["parsed", "gathered", "purged"]:
+ self.generate_commands()
+ self.run_commands()
+
+ if self.state == "purged":
+ wantd = {"disable": False}
+ haved = deepcopy(self.have)
+ if wantd != haved:
+ self.commands = ["delete high-availability"]
+ self.run_commands()
+ if "before" in self.result:
+ self._normalize_lists(self.result["before"])
+ if "after" in self.result:
+ self._normalize_lists(self.result["after"])
+ if "parsed" in self.result:
+ self._normalize_lists(self.result["parsed"])
+ return self.result
+
+ def generate_commands(self):
+ """Generate configuration commands to send based on
+ want, have and desired state.
+ """
+ wantd = deepcopy(self.want)
+ haved = deepcopy(self.have)
+ for entry in wantd, haved:
+ self._list_to_named_dict(entry)
+ self._normalize_lists(entry)
+
+ if self.state in ["deleted"]:
+ wantd, haved, p = self._prune_stubs(self._module.params.get("config", {}), haved)
+
+ if self.state in ["overridden"]:
+ wo = deepcopy(wantd)
+ self._diff_w_h(wo, haved)
+
+ haved_disable = haved.get("disable")
+
+ for k1, v1 in wo.items():
+
+ if not isinstance(v1, dict):
+ continue
+
+ for name, obj in v1.items():
+ if isinstance(obj, dict) and not obj:
+ wi, hi, pi = self._prune_stubs({k1: {name: {}}}, haved)
+ haved = hi
+
+ for k2, v2 in v1.items():
+ if not isinstance(v2, dict):
+ continue
+
+ for name, obj in v2.items():
+ if isinstance(obj, dict) and not obj:
+ wi, hi, pi = self._prune_stubs({k1: {k2: {name: {}}}}, haved)
+ haved = hi
+
+ if haved_disable is not None:
+ haved["disable"] = haved_disable
+
+ keys = set(wantd) | set(haved)
+
+ for k in keys:
+
+ want = wantd.get(k, {})
+ have = haved.get(k, {})
+
+ if k == "vrrp":
+ if self.state in ["merged"]:
+ want = combine(have, want, recursive=True, list_merge="append_rp")
+ self._compare_vrrp(want, have)
+
+ if k == "virtual_servers":
+ if self.state in ["merged"]:
+ want = combine(have, want, recursive=True)
+ self._compare_vsrvs(want, have)
+
+ if self.state in ["deleted"] and k == "disable":
+ want = have
+ if self.state in ["overridden"] and k == "disable" and not want:
+ want = False
+ if self.state in ["rendered"]:
+ have = None
+
+ self.compare(
+ parsers=self.parsers,
+ want={k: want},
+ have={k: have},
+ )
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ def _compare_vsrvs(self, want, have):
+ """Compare virtual servers.
+
+ Pre-index both want and have by (name, attribute) signature so that
+ each lookup is O(1) instead of O(n). Groups that are identical
+ between want and have are skipped entirely via an equality
+ short-circuit before leaf decomposition.
+ """
+ vs_parsers = [
+ "virtual_servers.address",
+ "virtual_servers.algorithm",
+ "virtual_servers.delay_loop",
+ "virtual_servers.forward_method",
+ "virtual_servers.persistence_timeout",
+ "virtual_servers.fwmark",
+ "virtual_servers.port",
+ "virtual_servers.protocol",
+ "virtual_servers.real_server.port",
+ "virtual_servers.real_server.health_check_script",
+ "virtual_servers.real_server.connection_timeout",
+ ]
+
+ want_index = (
+ {vs["name"]: vs for vs in want.values() if isinstance(vs, dict) and vs.get("name")}
+ if isinstance(want, dict)
+ else {}
+ )
+ have_index = (
+ {vs["name"]: vs for vs in have.values() if isinstance(vs, dict) and vs.get("name")}
+ if isinstance(have, dict)
+ else {}
+ )
+
+ all_names = set(want_index) | set(have_index)
+
+ for name in all_names:
+ w = want_index.get(name, {})
+ h = have_index.get(name, {})
+
+ if w == h and self.state not in ["rendered"]:
+ continue
+
+ wlist = self._extract_named_leafs(w) if w else []
+ hlist = self._extract_named_leafs(h) if h else []
+
+ if self.state == "rendered":
+ hlist = []
+
+ def _vsrv_sig(item):
+ if not isinstance(item, dict):
+ return None
+ iname = item.get("name")
+ if not iname:
+ return None
+ if "real_server" in item:
+ rs = item["real_server"]
+ if not isinstance(rs, dict) or "address" not in rs:
+ return None
+ addr = rs["address"]
+ for k in rs:
+ if k != "address":
+ return ("real_server", iname, addr, k)
+ return ("real_server", iname, addr, None)
+ for k in item:
+ if k != "name":
+ return ("attr", iname, k)
+ return None
+
+ have_leaf_index = {}
+ for hdict in hlist:
+ sig = _vsrv_sig(hdict)
+ if sig is not None:
+ have_leaf_index[sig] = hdict
+
+ want_leaf_index = {}
+ for wdict in wlist:
+ sig = _vsrv_sig(wdict)
+ if sig is not None:
+ want_leaf_index[sig] = wdict
+
+ if self.state in ["replaced", "deleted"]:
+ for sig, hdict in have_leaf_index.items():
+ wdict = want_leaf_index.get(sig, {})
+ if self.state == "deleted" and wdict:
+ wdict = {}
+ elif not wdict:
+ hdict = {}
+ self.compare(
+ parsers=vs_parsers,
+ want={"virtual_servers": wdict},
+ have={"virtual_servers": hdict},
+ )
+
+ if self.state in ["merged", "replaced", "rendered", "overridden"]:
+ for sig, wdict in want_leaf_index.items():
+ hdict = have_leaf_index.get(sig, {})
+ self.compare(
+ parsers=vs_parsers,
+ want={"virtual_servers": wdict},
+ have={"virtual_servers": hdict},
+ )
+
+ def _compare_vrrp(self, want, have):
+ """Compare VRRP groups and sync-groups.
+
+ Pre-index groups by name so matching is O(1). Groups that are
+ identical between want and have are skipped via equality
+ short-circuit before any leaf decomposition occurs — this is the
+ dominant performance win for large idempotent runs.
+ """
+ vrrp_parsers = [
+ "vrrp.snmp",
+ "vrrp.global_parameters",
+ "vrrp.global_parameters.garp",
+ "vrrp.groups",
+ "vrrp.groups.disable",
+ "vrrp.groups.no_preempt",
+ "vrrp.groups.rfc3768_compatibility",
+ "vrrp.groups.address",
+ "vrrp.groups.excluded_address",
+ "vrrp.groups.garp",
+ "vrrp.groups.authentication",
+ "vrrp.groups.transition_script",
+ "vrrp.groups.health_check",
+ "vrrp.groups.track.interface",
+ "vrrp.groups.track.exclude_vrrp_interface",
+ "vrrp.sync_groups.member",
+ "vrrp.sync_groups.transition_script",
+ "vrrp.sync_groups.health_check",
+ ]
+
+ if (
+ have.get("snmp") == "enabled"
+ and want.get("snmp") != "enabled"
+ and self.state not in ["deleted", "overridden"]
+ and (self.state != "merged" or "snmp" in want)
+ ):
+ self.commands.append("delete high-availability vrrp snmp")
+
+ non_named = {k: v for k, v in (want or {}).items() if k not in ("groups", "sync_groups")}
+ non_named_have = {
+ k: v for k, v in (have or {}).items() if k not in ("groups", "sync_groups")
+ }
+
+ hlist_non = self._extract_leaf_items(non_named_have)
+ wlist_non = self._extract_leaf_items(non_named)
+
+ if self.state == "rendered":
+ hlist_non = []
+
+ have_non_index = {}
+ for hdict in hlist_non:
+ sig = self._vrrp_leaf_sig(hdict)
+ have_non_index[sig] = hdict
+
+ want_non_index = {}
+ for wdict in wlist_non:
+ sig = self._vrrp_leaf_sig(wdict)
+ want_non_index[sig] = wdict
+
+ if self.state in ["replaced", "deleted"]:
+ for sig, hdict in have_non_index.items():
+ wdict = want_non_index.get(sig, {})
+ if self.state == "deleted" and wdict:
+ wdict = {}
+ if self.state == "replaced" and wdict and wdict != hdict:
+ wdict = {}
+ elif not wdict:
+ hdict = {}
+ self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict})
+
+ if self.state in ["merged", "replaced", "rendered", "overridden"]:
+ for sig, wdict in want_non_index.items():
+ hdict = have_non_index.get(sig, {})
+ self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict})
+
+ for section in ("groups", "sync_groups"):
+ want_objs = (want or {}).get(section, {})
+ have_objs = (have or {}).get(section, {})
+
+ if not isinstance(want_objs, dict):
+ want_objs = {}
+ if not isinstance(have_objs, dict):
+ have_objs = {}
+
+ all_names = set(want_objs) | set(have_objs)
+
+ for name in all_names:
+ w = want_objs.get(name, {})
+ h = have_objs.get(name, {})
+
+ if w == h and self.state not in ["rendered"]:
+ continue
+
+ wlist = self._extract_leaf_items({section: {name: w}}) if w else []
+ hlist = self._extract_leaf_items({section: {name: h}}) if h else []
+
+ if self.state == "rendered":
+ hlist = []
+
+ have_leaf_index = {}
+ for hdict in hlist:
+ sig = self._vrrp_leaf_sig(hdict)
+ have_leaf_index[sig] = hdict
+
+ want_leaf_index = {}
+ for wdict in wlist:
+ sig = self._vrrp_leaf_sig(wdict)
+ want_leaf_index[sig] = wdict
+
+ if self.state in ["replaced", "deleted"]:
+ for sig, hdict in have_leaf_index.items():
+ wdict = want_leaf_index.get(sig, {})
+ if self.state == "deleted" and wdict:
+ wdict = {}
+ if self.state == "replaced" and wdict and wdict != hdict:
+ wdict = {}
+ elif not wdict:
+ hdict = {}
+ self.compare(
+ parsers=vrrp_parsers,
+ want={"vrrp": wdict},
+ have={"vrrp": hdict},
+ )
+
+ if self.state in ["merged", "replaced", "rendered", "overridden"]:
+ for sig, wdict in want_leaf_index.items():
+ hdict = have_leaf_index.get(sig, {})
+ self.compare(
+ parsers=vrrp_parsers,
+ want={"vrrp": wdict},
+ have={"vrrp": hdict},
+ )
+
+ def _vrrp_leaf_sig(self, item):
+ """Build a hashable signature for a VRRP leaf dict for O(1) indexing."""
+ if not isinstance(item, dict) or not item:
+ return ()
+
+ container = next(iter(item))
+ inner = item[container]
+
+ sig = [container]
+
+ if isinstance(inner, dict) and "name" in inner:
+ sig.append(("name", inner["name"]))
+
+ if isinstance(inner, dict):
+ for k, v in inner.items():
+ if k == "name":
+ continue
+ if not isinstance(v, dict):
+ sig.append(k)
+ break
+ sig.append(k)
+ for leaf in v:
+ sig.append(leaf)
+ break
+ break
+
+ return tuple(sig)
+
+ def _list_to_named_dict(self, data):
+ """Convert all named-object lists to name-keyed dicts in-place.
+
+ Replaces the three separate _vrrp_groups_list_to_dict,
+ _vrrp_sync_groups_list_to_dict, and _virtual_servers_list_to_dict
+ methods with a single helper. Also normalises real_server lists
+ inside virtual servers.
+ """
+ # VRRP groups and sync_groups
+ vrrp = data.get("vrrp", {})
+ for key in ("groups", "sync_groups"):
+ items = vrrp.get(key)
+ if isinstance(items, list):
+ vrrp[key] = {
+ item["name"]: item
+ for item in items
+ if isinstance(item, dict) and item.get("name")
+ }
+
+ # Virtual servers
+ vss = data.get("virtual_servers")
+ if isinstance(vss, list):
+ new_vss = {}
+ for vs in vss:
+ if not isinstance(vs, dict):
+ continue
+ name = vs.get("name")
+ if not name:
+ continue
+ rs = vs.get("real_server")
+ if isinstance(rs, list):
+ vs["real_server"] = {
+ item["address"]: item
+ for item in rs
+ if isinstance(item, dict) and item.get("address")
+ }
+ new_vss[name] = vs
+ data["virtual_servers"] = new_vss
+ elif isinstance(vss, dict):
+ for vs in vss.values():
+ if not isinstance(vs, dict):
+ continue
+ rs = vs.get("real_server")
+ if isinstance(rs, list):
+ vs["real_server"] = {
+ item["address"]: item
+ for item in rs
+ if isinstance(item, dict) and item.get("address")
+ }
+
+ return data
+
+ def _extract_leaf_items(self, data, path=None, parent_name=None):
+ path = path or []
+ results = []
+
+ if isinstance(data, dict):
+ current_name = data.get("name", parent_name)
+
+ for k, v in data.items():
+ if k == "name" or (k == "snmp" and v == "disabled"):
+ continue
+ results.extend(self._extract_leaf_items(v, path + [k], current_name))
+ return results
+
+ leaf_key = path[-1]
+ top_key = path[0]
+
+ if top_key in ["groups", "sync_groups"]:
+ subkeys = path[2:]
+ else:
+ subkeys = path[1:]
+
+ nested = {leaf_key: data}
+
+ for p in reversed(subkeys[:-1]):
+ nested = {p: nested}
+ if parent_name:
+ out = {top_key: {"name": parent_name}}
+ out[top_key].update(nested)
+ else:
+ out = {top_key: nested}
+
+ results.append(out)
+ return results
+
+ def _normalize_lists(self, node):
+ """
+ Recursively normalize all lists inside a dict or list.
+ All lists are sorted to ensure consistent ordering for comparison.
+ """
+ if isinstance(node, dict):
+ for k, v in node.items():
+ if isinstance(v, list):
+ if all(not isinstance(i, (dict, list)) for i in v):
+ node[k] = sorted(v)
+ else:
+ for item in v:
+ self._normalize_lists(item)
+ elif isinstance(v, dict):
+ self._normalize_lists(v)
+ elif isinstance(node, list):
+ for item in node:
+ self._normalize_lists(item)
+
+ def _extract_named_leafs(self, data, parent_name=None, prefix_key=None):
+ results = []
+
+ if prefix_key == "real_server" and isinstance(data, dict):
+ for d, server_data in data.items():
+ if not isinstance(server_data, dict):
+ continue
+
+ address = server_data.get("address")
+ if not address:
+ continue
+
+ for k, v in server_data.items():
+ if k == "address":
+ continue
+
+ results.append(
+ {
+ "name": parent_name,
+ "real_server": {
+ "address": address,
+ k: v,
+ },
+ },
+ )
+ return results
+
+ if isinstance(data, dict):
+ current_name = data.get("name", parent_name)
+
+ for k, v in data.items():
+ if k == "name":
+ continue
+
+ results.extend(
+ self._extract_named_leafs(v, current_name, k),
+ )
+
+ return results
+
+ return [
+ {
+ "name": parent_name,
+ prefix_key: data,
+ },
+ ]
+
+ def _prune_stubs(self, w, h, path=""):
+ wc = {}
+ hc = self._remove_defaults(h)
+
+ if not self._remove_defaults(w) and remove_empties(hc):
+ self.commands = ["delete high-availability"]
+ return {}, {}, path
+
+ for k, wg in (self._remove_defaults(w) or {}).items():
+ next_path = f"{path} {k}".strip()
+ stub = self._cli_path(next_path)
+ hg = remove_empties(hc).get(k)
+
+ if hg is None:
+ continue
+
+ if not isinstance(wg, (dict, list)):
+ self.commands.append(f"delete high-availability {stub}")
+ hc.pop(k, None)
+ wc.pop(k, None)
+ continue
+
+ if not wg:
+ self.commands.append(f"delete high-availability {stub}")
+ hc.pop(k, None)
+ wc.pop(k, None)
+ continue
+
+ if isinstance(wg, list) and isinstance(hg, dict):
+ for item in wg:
+ name = item.get("name")
+ if not name:
+ continue
+
+ if name in hg:
+ self.commands.append(
+ f"delete high-availability {stub} {name}",
+ )
+
+ hg.pop(name, None)
+
+ if hg:
+ hc[k] = hg
+ else:
+ hc.pop(k, None)
+
+ if self._remove_defaults(wg):
+ wc[k] = wg
+ else:
+ wc.pop(k, None)
+
+ continue
+
+ if isinstance(wg, dict) and isinstance(hg, dict):
+ wi, hi, p = self._prune_stubs(wg, hg, next_path)
+
+ if wi:
+ wc[k] = wi
+
+ if hi:
+ hc[k] = hi
+ else:
+ hc.pop(k, None)
+
+ return wc, hc, path
+
+ def _remove_defaults(self, data):
+ """Strip None and False from config dicts, but preserve "disabled".
+
+ False is the argspec default for boolean flags (disable, no_preempt,
+ rfc3768_compatibility) and carries no config intent — stripping it
+ prevents spurious `delete` commands for fields already at their
+ default state.
+
+ "disabled" is an explicit user choice for snmp and must be preserved
+ so that _prune_stubs can act on it. The original code stripped it,
+ which made `snmp: disabled` invisible to the deleted-state logic.
+ """
+ if isinstance(data, dict):
+ cleaned = {}
+ for k, v in data.items():
+ if v is None or v is False:
+ continue
+ v = self._remove_defaults(v)
+ cleaned[k] = v
+ return cleaned
+ return data
+
+ def _cli_path(self, path):
+ token_map = {
+ "groups": "group",
+ "sync_groups": "sync-group",
+ "virtual_servers": "virtual-server",
+ }
+
+ parts = []
+ for p in path.split():
+ p = token_map.get(p, p)
+ parts.append(p.replace("_", "-"))
+
+ return " ".join(parts)
+
+ def _diff_w_h(self, w, h):
+
+ NAMED_OBJECT_KEYS = {
+ "groups",
+ "sync_groups",
+ "virtual_servers",
+ "global_parameters",
+ }
+
+ if not isinstance(w, dict) or not isinstance(h, dict):
+ return w
+
+ for key in w.keys() & h.keys():
+ wv = w[key]
+ hv = h[key]
+
+ if key in NAMED_OBJECT_KEYS and isinstance(wv, dict) and isinstance(hv, dict):
+ for name in wv.keys() & hv.keys():
+ if wv[name] != hv[name] and isinstance(wv[name], (dict, list)):
+ wv[name] = {}
+ elif wv[name] != hv[name]:
+ wv[name] = None
+ continue
+ self._diff_w_h(wv, hv)
+ return w
diff --git a/plugins/module_utils/network/vyos/config/hostname/hostname.py b/plugins/module_utils/network/vyos/config/hostname/hostname.py
index 36aba74f..8b30a693 100644
--- a/plugins/module_utils/network/vyos/config/hostname/hostname.py
+++ b/plugins/module_utils/network/vyos/config/hostname/hostname.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py
index 0a55564e..71e4c0d1 100644
--- a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py
+++ b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py
@@ -11,7 +11,6 @@ created
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from copy import deepcopy
@@ -75,6 +74,7 @@ class Interfaces(ConfigBase):
if self.state in self.ACTION_STATES:
existing_interfaces_facts = self.get_interfaces_facts()
+
else:
existing_interfaces_facts = []
diff --git a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py
index a23494cd..cfce6fee 100644
--- a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py
+++ b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py
@@ -13,7 +13,6 @@ created
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
@@ -256,6 +255,10 @@ class L3_interfaces(ConfigBase):
def _state_deleted(self, want, have):
"""The command generator when state is deleted
+ Deletes only the L3 address attributes (base interface and VIFs)
+ owned by this module, never the interface subtree, so L2 settings
+ are preserved.
+
:rtype: A list
:returns: the commands necessary to remove the current configuration
of the provided objects
@@ -266,14 +269,33 @@ class L3_interfaces(ConfigBase):
if have_copy is not None:
if all(v in (None, {}, []) for k, v in want_copy.items() if k != "name"):
- commands.append(
- self._compute_commands(
- key=None,
- value=None,
- interface=want_copy["name"],
- remove=True,
- ),
- )
+ # Only delete L3 attributes we own — do not touch L2 config
+ have_vifs = have_copy.pop("vifs", []) or []
+
+ for addr_family in ("ipv4", "ipv6"):
+ for addr in have_copy.get(addr_family) or []:
+ commands.append(
+ self._compute_commands(
+ key="address",
+ value=addr["address"],
+ interface=want_copy["name"],
+ remove=True,
+ ),
+ )
+
+ for have_vif in have_vifs:
+ for addr_family in ("ipv4", "ipv6"):
+ for addr in have_vif.get(addr_family) or []:
+ commands.append(
+ self._compute_commands(
+ key="address",
+ value=addr["address"],
+ interface=want_copy["name"],
+ vif=have_vif["vlan_id"],
+ remove=True,
+ ),
+ )
+
return commands
want_vifs = want_copy.pop("vifs", [])
diff --git a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py
index 62270503..203519e4 100644
--- a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py
+++ b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py
@@ -8,8 +8,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py
index 39724f1b..82a35a54 100644
--- a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py
+++ b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py
@@ -8,8 +8,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py
index 97fcbce5..36bf1b2d 100644
--- a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py
+++ b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py
@@ -13,7 +13,6 @@ created
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
diff --git a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py
index 35a2624a..1724f338 100644
--- a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py
+++ b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
@@ -32,6 +31,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import (
Logging_globalTemplate,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import (
+ Logging_globalTemplate15,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import (
+ LooseVersion,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version
class Logging_global(ResourceModule):
@@ -47,22 +53,25 @@ class Logging_global(ResourceModule):
resource="logging_global",
tmplt=Logging_globalTemplate(),
)
- self.parsers = [
- "console.facilities",
- "global_params.archive.file_num",
- "global_params.archive.size",
- "global_params.marker_interval",
- "global_params.preserve_fqdn",
- "global_params.facilities",
- "files.archive.size",
- "files.archive.file_num",
- "files",
- "hosts.port",
- "hosts.facility.protocol", # 1.3 and below
- "hosts.protocol",
- "hosts",
- "users",
- ]
+
+ def _validate_template(self):
+ version = get_os_version(self._module)
+ if LooseVersion(version) >= LooseVersion("1.5"):
+ self._tmplt = Logging_globalTemplate15()
+ else:
+ self._tmplt = Logging_globalTemplate()
+
+ self.parsers = [p["name"] for p in self._tmplt.PARSERS if not p["name"].endswith(".state")]
+
+ def parse(self):
+ """override parse to check template"""
+ self._validate_template()
+ return super().parse()
+
+ def get_parser(self, name):
+ """get_parsers"""
+ self._validate_template()
+ return super().get_parser(name)
def execute_module(self):
"""Execute the module
@@ -70,11 +79,31 @@ class Logging_global(ResourceModule):
:rtype: A dictionary
:returns: The result from module execution
"""
+ self._validate_template()
if self.state not in ["parsed", "gathered"]:
self.generate_commands()
self.run_commands()
return self.result
+ def _strip_unsupported_15(self, data):
+ """Remove 1.4-only keys from a list_to_dict result for 1.5 devices."""
+ if not data:
+ return data
+ warnings = []
+ for key in ("files", "users"):
+ if data.pop(key, None) is not None:
+ warnings.append(
+ "'{0}' is not supported on VyOS 1.5+, ignoring.".format(key),
+ )
+ if "global_params" in data:
+ if data["global_params"].pop("archive", None) is not None:
+ warnings.append(
+ "'global_params.archive' is not supported on VyOS 1.5+, ignoring.",
+ )
+ for warning in warnings:
+ self._module.warn(warning)
+ return data
+
def generate_commands(self):
"""Generate configuration commands to send based on
want, have and desired state.
@@ -88,6 +117,11 @@ class Logging_global(ResourceModule):
else:
haved = dict()
+ version = get_os_version(self._module)
+ if LooseVersion(version) >= LooseVersion("1.5"):
+ wantd = self._strip_unsupported_15(wantd)
+ haved = self._strip_unsupported_15(haved)
+
if self.state in ["overridden", "replaced"]:
if wantd != haved:
wantx, havex = self.call_op(wantd, haved, "overridden")
diff --git a/plugins/module_utils/network/vyos/config/nat/__init__.py b/plugins/module_utils/network/vyos/config/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/nat/nat.py b/plugins/module_utils/network/vyos/config/nat/nat.py
new file mode 100644
index 00000000..a2800914
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/nat/nat.py
@@ -0,0 +1,577 @@
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import (
+ NatTemplate,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine
+
+
+class Nat(ResourceModule):
+ """The vyos_nat config class"""
+
+ def __init__(self, module):
+ super(Nat, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="nat",
+ tmplt=NatTemplate(),
+ )
+ self.parsers = []
+
+ def execute_module(self):
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ wantd = deepcopy(self.want)
+ haved = deepcopy(self.have)
+
+ if self.state == "merged":
+ wantd = combine(haved, wantd, recursive=True)
+
+ if self.state == "deleted":
+ if not wantd:
+ for nat_type in haved:
+ self.commands.append(f"delete {nat_type}")
+ return
+ self._list_to_dict(wantd)
+ self._list_to_dict(haved)
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ return
+
+ self._list_to_dict(wantd)
+ self._list_to_dict(haved)
+
+ if self.state == "replaced":
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ self._set_commands(wantd, haved)
+ elif self.state == "overridden":
+ self._delete_nat_objects(wantd, haved, only_missing=True)
+ self._delete_nat_objects(wantd, haved, only_missing=False)
+ self._set_commands(wantd, haved)
+ else:
+ self._set_commands(wantd, haved)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------------
+ # List → keyed dict conversion
+ # -------------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ nat = config.get("nat", {})
+ cgnat = nat.get("cgnat", {})
+
+ pool = cgnat.get("pool", {})
+ for ptype in ("external", "internal"):
+ entries = pool.get(ptype)
+ if isinstance(entries, list):
+ pool[ptype] = {item["name"]: item for item in entries}
+
+ rules = cgnat.get("rule")
+ if isinstance(rules, list):
+ cgnat["rule"] = {r["id"]: r for r in rules}
+
+ for section in ("destination", "source", "static"):
+ rules = nat.get(section, {}).get("rule")
+ if isinstance(rules, list):
+ nat[section]["rule"] = {r["id"]: r for r in rules}
+
+ nat64 = config.get("nat64", {})
+ rules = nat64.get("source", {}).get("rule")
+ if isinstance(rules, list):
+ nat64["source"]["rule"] = {r["id"]: r for r in rules}
+ for rule in nat64["source"]["rule"].values():
+ pools = rule.get("translation", {}).get("pool")
+ if isinstance(pools, list):
+ rule["translation"]["pool"] = {p["id"]: p for p in pools}
+
+ nat66 = config.get("nat66", {})
+ for section in ("destination", "source"):
+ rules = nat66.get(section, {}).get("rule")
+ if isinstance(rules, list):
+ nat66[section]["rule"] = {r["id"]: r for r in rules}
+
+ # -------------------------------------------------------------------------
+ # Top-level dispatch
+ # -------------------------------------------------------------------------
+
+ def _set_commands(self, wantd, haved):
+ self._compare_cgnat_global(wantd, haved)
+ self._compare_cgnat_pools(wantd, haved)
+ self._compare_cgnat_rules(wantd, haved)
+
+ for section in ("destination", "source", "static"):
+ self._compare_nat_rules("nat", section, wantd, haved)
+
+ self._compare_nat_rules("nat64", "source", wantd, haved)
+
+ for section in ("destination", "source"):
+ self._compare_nat_rules("nat66", section, wantd, haved)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------------
+ # Delete helpers
+ # -------------------------------------------------------------------------
+
+ def _delete_nat_objects(self, wantd, haved, only_missing=False):
+ """
+ Generate delete commands for NAT objects.
+ only_missing=False: delete objects present in both want and have (when different)
+ only_missing=True: delete objects present in have but absent from want
+ """
+ for nat_type in haved:
+ want_nat = wantd.get(nat_type, {})
+ have_nat = haved[nat_type]
+
+ if only_missing and nat_type not in wantd:
+ self.commands.append(f"delete {nat_type}")
+ continue
+
+ for section in have_nat:
+ want_section = want_nat.get(section, {})
+ have_section = have_nat[section]
+
+ if only_missing and section not in want_nat:
+ self.commands.append(
+ f"delete {nat_type} {section.replace('_', '-')}",
+ )
+ continue
+
+ if section == "cgnat":
+ for pool_type in ("external", "internal"):
+ want_pools = want_section.get("pool", {}).get(pool_type, {})
+ have_pools = have_section.get("pool", {}).get(pool_type, {})
+ for name in have_pools:
+ if only_missing and name not in want_pools:
+ self.commands.append(
+ f"delete {nat_type} cgnat pool {pool_type} {name}",
+ )
+
+ elif not only_missing and name in want_pools:
+ if self.state == "deleted" or want_pools[name] != have_pools[name]:
+ self.commands.append(
+ f"delete {nat_type} cgnat pool {pool_type} {name}",
+ )
+ want_rules = want_section.get("rule", {})
+ have_rules = have_section.get("rule", {})
+ for rid in have_rules:
+ if only_missing and rid not in want_rules:
+ self.commands.append(f"delete {nat_type} cgnat rule {rid}")
+
+ elif not only_missing and rid in want_rules:
+ if self.state == "deleted" or want_rules[rid] != have_rules[rid]:
+ self.commands.append(f"delete {nat_type} cgnat rule {rid}")
+ else:
+ want_rules = want_section.get("rule", {})
+ have_rules = have_section.get("rule", {})
+ cli_section = section.replace("_", "-")
+ for rid in have_rules:
+ if only_missing and rid not in want_rules:
+ self.commands.append(
+ f"delete {nat_type} {cli_section} rule {rid}",
+ )
+
+ elif not only_missing and rid in want_rules:
+ if self.state == "deleted" or want_rules[rid] != have_rules[rid]:
+ self.commands.append(
+ f"delete {nat_type} {cli_section} rule {rid}",
+ )
+
+ # -------------------------------------------------------------------------
+ # CGNAT
+ # -------------------------------------------------------------------------
+
+ def _compare_cgnat_global(self, wantd, haved):
+ if self.state in ("replaced", "overridden") and not wantd.get("nat", {}).get("cgnat"):
+ return
+ w = wantd.get("nat", {}).get("cgnat", {}).get("log_allocation")
+ h = haved.get("nat", {}).get("cgnat", {}).get("log_allocation")
+ if bool(w) != bool(h):
+ self.addcmd(
+ {"nat": {"cgnat": {"log_allocation": True}}},
+ "cgnat_log_allocation",
+ not bool(w),
+ )
+
+ def _compare_cgnat_pools(self, wantd, haved):
+ want_ext = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {})
+ have_ext = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {})
+ want_int = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {})
+ have_int = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {})
+
+ scope = self.state in ("replaced", "overridden")
+ ext_names = set(want_ext) if scope else set(want_ext) | set(have_ext)
+ int_names = set(want_int) if scope else set(want_int) | set(have_int)
+
+ for name in ext_names:
+ w = want_ext.get(name, {})
+ h = have_ext.get(name, {})
+ if scope and w != h:
+ h = {}
+ self._compare_external_pool(name, w, h)
+
+ for name in int_names:
+ w = want_int.get(name, {})
+ h = have_int.get(name, {})
+ if scope and w != h:
+ h = {}
+ self._compare_internal_pool(name, w, h)
+
+ def _compare_external_pool(self, name, want, have):
+ w = want.get("external_port_range")
+ h = have.get("external_port_range")
+ if w != h:
+ if w:
+ self.addcmd({"name": name, "range": w}, "cgnat_pool_external_port_range", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"name": name, "range": h}, "cgnat_pool_external_port_range", True)
+
+ w = want.get("per_user_limit", {}).get("port")
+ h = have.get("per_user_limit", {}).get("port")
+ if w != h:
+ if w:
+ self.addcmd({"name": name, "limit": w}, "cgnat_pool_external_per_user", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"name": name, "limit": h}, "cgnat_pool_external_per_user", True)
+
+ want_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in want.get("range", [])}
+ have_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in have.get("range", [])}
+ for val, rng in want_ranges.items():
+ existing = have_ranges.get(val)
+ if existing is None or existing != rng:
+ seq = rng.get("seq") if isinstance(rng, dict) else None
+ self.addcmd(
+ {"name": name, "range": val, "seq": seq},
+ "cgnat_pool_external_range",
+ False,
+ )
+
+ if self.state in ("replaced", "overridden"):
+ for val in have_ranges:
+ if val not in want_ranges:
+ self.addcmd({"name": name, "range": val}, "cgnat_pool_external_range", True)
+
+ def _compare_internal_pool(self, name, want, have):
+ want_ranges = set(want.get("range", []))
+ have_ranges = set(have.get("range", []))
+
+ for rng in want_ranges - have_ranges:
+ self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", False)
+
+ if self.state in ("replaced", "overridden"):
+ for rng in have_ranges - want_ranges:
+ self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", True)
+
+ def _compare_cgnat_rules(self, wantd, haved):
+ want_rules = wantd.get("nat", {}).get("cgnat", {}).get("rule", {})
+ have_rules = haved.get("nat", {}).get("cgnat", {}).get("rule", {})
+
+ rids = (
+ set(want_rules)
+ if self.state in ("replaced", "overridden")
+ else set(want_rules) | set(have_rules)
+ )
+
+ for rid in rids:
+ w = want_rules.get(rid, {})
+ h = have_rules.get(rid, {})
+
+ if self.state in ("replaced", "overridden") and w != h:
+ h = {}
+
+ w_src = w.get("source", {}).get("pool")
+ h_src = h.get("source", {}).get("pool")
+ if w_src != h_src:
+ if w_src:
+ self.addcmd({"id": rid, "pool": w_src}, "cgnat_rule_source_pool", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"id": rid, "pool": h_src}, "cgnat_rule_source_pool", True)
+
+ w_tr = w.get("translation", {}).get("pool")
+ h_tr = h.get("translation", {}).get("pool")
+ if w_tr != h_tr:
+ if w_tr:
+ self.addcmd({"id": rid, "pool": w_tr}, "cgnat_rule_translation_pool", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd({"id": rid, "pool": h_tr}, "cgnat_rule_translation_pool", True)
+
+ # -------------------------------------------------------------------------
+ # NAT / NAT64 / NAT66 rules
+ # -------------------------------------------------------------------------
+
+ def _compare_nat_rules(self, nat_type, section, wantd, haved):
+ want_rules = wantd.get(nat_type, {}).get(section, {}).get("rule", {})
+ have_rules = haved.get(nat_type, {}).get(section, {}).get("rule", {})
+
+ rids = (
+ set(want_rules)
+ if self.state in ("replaced", "overridden")
+ else set(want_rules) | set(have_rules)
+ )
+
+ for rid in rids:
+ w = want_rules.get(rid, {})
+ h = have_rules.get(rid, {})
+ if self.state in ("replaced", "overridden") and w != h:
+ h = {}
+ if w == h and self.state != "rendered":
+ continue
+ self._compare_rule(nat_type, section, rid, w, h)
+
+ def _compare_rule(self, nat_type, section, rid, want, have):
+ ctx = {"nat": nat_type, "type": section, "id": rid}
+
+ want_lb = want.get("load_balance") or {}
+ have_lb = have.get("load_balance") or {}
+ want_trans_addr = (want.get("translation") or {}).get("address")
+ have_trans_addr = (have.get("translation") or {}).get("address")
+ if want_lb and want_trans_addr is not None:
+ self._module.fail_json(
+ msg="translation.address and load_balance are mutually exclusive",
+ )
+ if self.state == "merged":
+ if want_lb and have_trans_addr is not None:
+ self._module.fail_json(
+ msg=(
+ "Cannot add load_balance to a rule that already has translation.address with "
+ "state=merged; use state=replaced or state=overridden"
+ ),
+ )
+ if want_trans_addr is not None and have_lb:
+ self._module.fail_json(
+ msg=(
+ "Cannot add translation.address to a rule that already has load_balance with "
+ "state=merged; use state=replaced or state=overridden"
+ ),
+ )
+
+ for field in set(want) | set(have):
+ if field == "inbound_interface":
+ continue
+ val = want.get(field) if field in want else have.get(field)
+ if isinstance(val, bool):
+ self._cmp_bool(want, have, field, ctx, f"nat_type_{field}")
+ elif isinstance(val, str):
+ self._cmp_scalar(want, have, field, ctx, f"nat_type_{field}")
+
+ self._cmp_interface(want, have, ctx, nat_type, section)
+ self._cmp_outbound_interface(want, have, ctx)
+ for atype in ("destination", "source"):
+ self._cmp_addr_sub(want, have, atype, ctx)
+ self._cmp_translation(want, have, ctx)
+ self._cmp_match_mark(want, have, ctx)
+ self._cmp_nat64_pools(want, have, ctx)
+ self._cmp_load_balance(want, have, ctx)
+
+ # -------------------------------------------------------------------------
+ # Field-level helpers
+ # -------------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser):
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h:
+ if w:
+ self.addcmd(dict(ctx), parser, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx), parser, True)
+
+ def _cmp_interface(self, want, have, ctx, nat_type, section):
+ iface_w = want.get("inbound_interface")
+ iface_h = have.get("inbound_interface")
+ if iface_w == iface_h:
+ return
+
+ if nat_type == "nat" and section == "static":
+ if iface_w:
+ self.addcmd(dict(ctx, value=iface_w), "nat_static_inbound_interface", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h), "nat_static_inbound_interface", True)
+ return
+
+ iface_w = iface_w or {}
+ iface_h = iface_h or {}
+
+ if nat_type == "nat":
+ parser_name = "nat_inbound_interface_name"
+ parser_group = "nat_inbound_interface_group"
+ else:
+ parser_name = "nat6x_inbound_interface"
+ parser_group = "nat6x_inbound_interface"
+
+ if iface_w.get("name") != iface_h.get("name"):
+ if iface_w.get("name"):
+ self.addcmd(dict(ctx, value=iface_w["name"]), parser_name, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["name"]), parser_name, True)
+
+ if nat_type == "nat" and iface_w.get("group") != iface_h.get("group"):
+ if iface_w.get("group"):
+ self.addcmd(dict(ctx, value=iface_w["group"]), parser_group, False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["group"]), parser_group, True)
+
+ def _cmp_outbound_interface(self, want, have, ctx):
+ iface_w = want.get("outbound_interface") or {}
+ iface_h = have.get("outbound_interface") or {}
+
+ if iface_w.get("name") != iface_h.get("name"):
+ if iface_w.get("name"):
+ self.addcmd(dict(ctx, value=iface_w["name"]), "nat_type_outbound_interface", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, value=iface_h["name"]), "nat_type_outbound_interface", True)
+
+ if iface_w.get("group") != iface_h.get("group"):
+ if iface_w.get("group"):
+ self.addcmd(
+ dict(ctx, value=iface_w["group"]),
+ "nat_type_outbound_interface_group",
+ False,
+ )
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(
+ dict(ctx, value=iface_h["group"]),
+ "nat_type_outbound_interface_group",
+ True,
+ )
+
+ def _cmp_addr_sub(self, want, have, atype, ctx):
+ sub_w = want.get(atype) or {}
+ sub_h = have.get(atype) or {}
+ if sub_w == sub_h:
+ return
+
+ changed = {k: v for k, v in sub_w.items() if sub_h.get(k) != v}
+ removed = {
+ k: v
+ for k, v in sub_h.items()
+ if k not in sub_w and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(dict(ctx, atype=atype, sub=changed), "nat_type_address", False)
+ if removed:
+ self.addcmd(dict(ctx, atype=atype, sub=removed), "nat_type_address", True)
+
+ def _cmp_translation(self, want, have, ctx):
+ trans_w = want.get("translation") or {}
+ trans_h = have.get("translation") or {}
+ if trans_w == trans_h:
+ return
+
+ changed = {k: v for k, v in trans_w.items() if k != "pool" and trans_h.get(k) != v}
+ removed = {
+ k: v
+ for k, v in trans_h.items()
+ if k != "pool" and k not in trans_w and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(dict(ctx, translation=changed), "nat_type_translation_address", False)
+ if removed:
+ self.addcmd(dict(ctx, translation=removed), "nat_type_translation_address", True)
+
+ def _cmp_match_mark(self, want, have, ctx):
+ w = want.get("match", {}).get("mark")
+ h = have.get("match", {}).get("mark")
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, mark=w), "nat64_match_mark", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, mark=h), "nat64_match_mark", True)
+
+ def _cmp_nat64_pools(self, want, have, ctx):
+ want_pools = want.get("translation", {}).get("pool", {})
+ have_pools = have.get("translation", {}).get("pool", {})
+
+ if isinstance(want_pools, list):
+ want_pools = {p["id"]: p for p in want_pools}
+ if isinstance(have_pools, list):
+ have_pools = {p["id"]: p for p in have_pools}
+
+ for pid in set(want_pools) | set(have_pools):
+ wp = want_pools.get(pid, {})
+ hp = have_pools.get(pid, {})
+
+ if wp == hp:
+ continue
+
+ changed = {k: v for k, v in wp.items() if k != "id" and hp.get(k) != v}
+ removed = {
+ k: v
+ for k, v in hp.items()
+ if k != "id" and k not in wp and self.state in ("replaced", "overridden")
+ }
+
+ if changed:
+ self.addcmd(
+ dict(ctx, pool_id=pid, pool=changed),
+ "nat64_translation_pool",
+ False,
+ )
+ if removed:
+ self.addcmd(
+ dict(ctx, pool_id=pid, pool=removed),
+ "nat64_translation_pool",
+ True,
+ )
+
+ def _cmp_load_balance(self, want, have, ctx):
+ lb_w = want.get("load_balance") or {}
+ lb_h = have.get("load_balance") or {}
+
+ want_hash = set(lb_w.get("hash") or [])
+ have_hash = set(lb_h.get("hash") or [])
+
+ for h in want_hash - have_hash:
+ self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", False)
+ if self.state in ("replaced", "overridden"):
+ for h in have_hash - want_hash:
+ self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", True)
+
+ want_backends = lb_w.get("backend", [])
+ have_backends = lb_h.get("backend", [])
+ if isinstance(want_backends, list):
+ want_backends = {b["ip"]: b for b in want_backends}
+ if isinstance(have_backends, list):
+ have_backends = {b["ip"]: b for b in have_backends}
+
+ for ip in set(want_backends) | set(have_backends):
+ wb = want_backends.get(ip, {})
+ hb = have_backends.get(ip, {})
+ if wb == hb:
+ continue
+ if wb:
+ weight = wb.get("weight")
+ if weight is None:
+ self._module.fail_json(msg="load_balance.backend entries require 'weight'")
+ self.addcmd(dict(ctx, ip=ip, weight=weight), "nat_type_lb_backend", False)
+ elif self.state in ("replaced", "overridden"):
+ self.addcmd(dict(ctx, ip=ip, weight=hb.get("weight")), "nat_type_lb_backend", True)
diff --git a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py
index 06ec1d6b..78b7a545 100644
--- a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py
+++ b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py
index 837a2ddf..bc93ac24 100644
--- a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py
+++ b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
The vyos_ospf_interfaces config file.
diff --git a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py
index ed973179..20821980 100644
--- a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py
+++ b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py
@@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -57,7 +57,7 @@ class Ospfv2(ConfigBase):
:returns: The current configuration as a dictionary
"""
- (facts, _warnings) = Facts(self._module).get_facts(
+ facts, _warnings = Facts(self._module).get_facts(
self.gather_subset,
self.gather_network_resources,
data=data,
diff --git a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py
index 30d30571..a84899f5 100644
--- a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py
+++ b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py
@@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set
necessary to bring the current configuration to it's desired end-state is
created
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py
index 9b703399..05164c31 100644
--- a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py
+++ b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py
index dca99d1c..948341d3 100644
--- a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py
+++ b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py
index 4bc5ddc4..cdc1d6e1 100644
--- a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py
+++ b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py
index da8f8e3d..2a09c0f0 100644
--- a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py
+++ b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py
@@ -13,7 +13,6 @@ created
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from copy import deepcopy
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..2d986cb4
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,665 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_vpn_ipsec config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+
+Follows the established per-module convention used by vyos_ha/vyos_nat
+(list-to-dict conversion + explicit per-state branching in
+generate_commands), rather than a shared generic engine.
+
+State semantics (standard Ansible RM convention, confirmed against a
+real device run that caught a bug in an earlier version of this file):
+ - merged: only items/fields named in `want` are touched. Nothing
+ absent from `want` is ever deleted.
+ - replaced: only items NAMED in `want` are touched (same item scope
+ as merged) -- but for each named item, its full state is
+ reconciled to exactly match `want` (fields present in
+ `have` but omitted from `want` ARE deleted). Items not
+ named in `want` at all are left completely alone.
+ - overridden: every item is in scope, including ones absent from
+ `want` entirely -- those get deleted wholesale. Named
+ items are reconciled the same way as `replaced`.
+
+This is implemented via two independent flags:
+ - select_all: whether item iteration considers have-only items too
+ (True only for overridden; False for merged/replaced).
+ - reconcile: whether omitted fields within an already-selected item
+ get deleted (True for replaced/overridden; False for
+ merged/rendered).
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
+ Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsec(ResourceModule):
+ """
+ The vyos_vpn_ipsec config class
+ """
+
+ def __init__(self, module):
+ super(Vpn_ipsec, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vpn_ipsec",
+ tmplt=Vpn_ipsecTemplate(),
+ )
+ self.parsers = [
+ "esp_group",
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ "esp_group.compression",
+ "esp_group.disable_rekey",
+ "esp_group.life_bytes",
+ "esp_group.life_packets",
+ "esp_group.lifetime",
+ "esp_group.mode",
+ "esp_group.pfs",
+ "ike_group",
+ "ike_group.key_exchange",
+ "ike_group.proposal",
+ "ike_group.proposal.dh_group",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.close_action",
+ "ike_group.dead_peer_detection.action",
+ "ike_group.dead_peer_detection.interval",
+ "ike_group.dead_peer_detection.timeout",
+ "ike_group.disable_mobike",
+ "ike_group.ikev2_reauth",
+ "ike_group.lifetime",
+ "ike_group.mode",
+ "profile",
+ "profile.authentication.mode",
+ "profile.authentication.pre_shared_secret",
+ "profile.esp_group",
+ "profile.ike_group",
+ "profile.disable",
+ "authentication.psk.secret_type",
+ "authentication.psk.dhcp_interface",
+ "authentication.ppk",
+ "authentication.ppk.id",
+ "authentication.ppk.secret",
+ "authentication.ppk.secret_type",
+ "interface",
+ "log.level",
+ "log.subsystem",
+ "options.disable_route_autoinstall",
+ "options.flexvpn",
+ "options.interface",
+ "options.retransmission.attempts",
+ "options.retransmission.base",
+ "options.retransmission.timeout",
+ "options.virtual_ip",
+ "disable_uniqreqids",
+ ]
+
+ def execute_module(self):
+ """Execute the module
+
+ :rtype: A dictionary
+ :returns: The result from module execution
+ """
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ """Generate configuration commands to send based on
+ want, have and desired state.
+ """
+ wantd = deepcopy(self.want) or {}
+ haved = deepcopy(self.have) or {}
+
+ for entry in (wantd, haved):
+ self._list_to_dict(entry)
+
+ scoped_delete = None
+ if self.state == "deleted":
+ if wantd:
+ # user named specific items -- surgical removal of just
+ # those, everything else preserved (vyos_vrf precedent:
+ # deleted + instances:[{name: vrf-blue}] removes only
+ # vrf-blue). Capture what was named before wiping wantd.
+ scoped_delete = wantd
+ wantd = {}
+
+ if self.state == "merged":
+ # NOTE: list_to_dict() above must run BEFORE this. dict_merge
+ # concatenates lists rather than merging matching entries by
+ # key, so merging while ike_group/esp_group/etc are still
+ # lists would duplicate entries instead of filling in omitted
+ # fields from `have`. Once they're name-keyed dicts, dict_merge
+ # recurses per-key correctly, which is what lets a partial
+ # update (e.g. specifying only key_exchange) leave other
+ # existing fields on that same group untouched.
+ wantd = dict_merge(haved, wantd)
+
+ select_all = self.state in ("overridden", "deleted")
+ reconcile = self.state in ("replaced", "overridden", "deleted")
+
+ self._compare_esp_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ike_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_profiles(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_psks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ppks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_top_level(wantd, haved, select_all, reconcile, scoped_delete)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------
+ # List -> name-keyed dict conversion (matches vyos_ha/vyos_nat style)
+ # -------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ for key in ("ike_group", "esp_group", "profile"):
+ items = config.get(key)
+ if isinstance(items, list):
+ config[key] = {item["name"]: item for item in items}
+ for item in config[key].values():
+ if isinstance(item.get("proposal"), list):
+ item["proposal"] = {p["proposal_id"]: p for p in item["proposal"]}
+
+ auth = config.get("authentication", {})
+ for key in ("psk", "ppk"):
+ items = auth.get(key)
+ if isinstance(items, list):
+ auth[key] = {item["name"]: item for item in items}
+
+ # -------------------------------------------------------------------
+ # ESP groups
+ # -------------------------------------------------------------------
+
+ def _compare_esp_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("esp_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("esp_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("esp_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ # only reached when select_all (overridden): item entirely
+ # absent from want -> delete wholesale
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "esp_group", False)
+
+ for field in ("mode", "pfs", "lifetime", "life_bytes", "life_packets"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("compression", "disable_rekey"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ None,
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # IKE groups
+ # -------------------------------------------------------------------
+
+ def _compare_ike_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("ike_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("ike_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("ike_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "ike_group", False)
+
+ self._cmp_scalar(
+ w,
+ h,
+ "key_exchange",
+ {"name": name},
+ "ike_group.key_exchange",
+ reconcile,
+ )
+ for field in ("close_action", "lifetime", "mode"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("disable_mobike", "ikev2_reauth"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+
+ w_dpd = w.get("dead_peer_detection", {})
+ h_dpd = h.get("dead_peer_detection", {})
+ for field in ("action", "interval", "timeout"):
+ self._cmp_scalar(
+ w_dpd,
+ h_dpd,
+ field,
+ {"name": name},
+ "ike_group.dead_peer_detection.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "ike_group.proposal",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.proposal.dh_group",
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Proposals (shared by esp_group / ike_group)
+ # -------------------------------------------------------------------
+
+ def _compare_proposals(
+ self,
+ want_props,
+ have_props,
+ group_ctx,
+ bare_parser,
+ encryption_parser,
+ hash_parser,
+ dh_group_parser,
+ reconcile,
+ ):
+ # a proposal collection lives entirely inside an already-selected
+ # group -- once that group is in scope, its own proposals always
+ # get full reconciliation under replaced/overridden (never a
+ # separate select_all concern of their own).
+ ids = set(want_props) | set(have_props) if reconcile else set(want_props)
+ for pid in ids:
+ w = want_props.get(pid, {})
+ h = have_props.get(pid, {})
+ if w == h:
+ continue
+
+ if pid in have_props and pid not in want_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, True)
+ continue
+
+ if pid not in have_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, False)
+
+ ctx = dict(group_ctx, proposal_id=pid)
+ self._cmp_scalar(w, h, "encryption", ctx, encryption_parser, reconcile)
+ self._cmp_scalar(w, h, "hash", ctx, hash_parser, reconcile)
+ if dh_group_parser:
+ self._cmp_scalar(w, h, "dh_group", ctx, dh_group_parser, reconcile)
+
+ # -------------------------------------------------------------------
+ # Profiles
+ # -------------------------------------------------------------------
+
+ def _compare_profiles(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_profiles = haved.get("profile", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("profile", {})):
+ if name in have_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ return
+
+ want_profiles = wantd.get("profile", {})
+ names = set(want_profiles) | set(have_profiles) if select_all else set(want_profiles)
+
+ for name in names:
+ w = want_profiles.get(name, {})
+ h = have_profiles.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_profiles and name not in want_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ continue
+
+ if name not in have_profiles:
+ self.addcmd({"name": name}, "profile", False)
+
+ ctx = {"name": name}
+ w_auth = w.get("authentication", {})
+ h_auth = h.get("authentication", {})
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "mode",
+ ctx,
+ "profile.authentication.mode",
+ reconcile,
+ )
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "pre_shared_secret",
+ ctx,
+ "profile.authentication.pre_shared_secret",
+ reconcile,
+ )
+ self._cmp_scalar(w, h, "esp_group", ctx, "profile.esp_group", reconcile)
+ self._cmp_scalar(w, h, "ike_group", ctx, "profile.ike_group", reconcile)
+ self._cmp_bool(w, h, "disable", ctx, "profile.disable", reconcile)
+
+ w_tunnels = set(w.get("bind_tunnel") or [])
+ h_tunnels = set(h.get("bind_tunnel") or [])
+ for tun in w_tunnels - h_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", False)
+ if reconcile:
+ for tun in h_tunnels - w_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", True)
+
+ # -------------------------------------------------------------------
+ # PSKs
+ # -------------------------------------------------------------------
+
+ def _compare_psks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_psks = haved.get("authentication", {}).get("psk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("psk", {})):
+ if name in have_psks:
+ self.commands.append(
+ "delete vpn ipsec authentication psk {0}".format(name),
+ )
+ return
+
+ want_psks = wantd.get("authentication", {}).get("psk", {})
+ names = set(want_psks) | set(have_psks) if select_all else set(want_psks)
+
+ for name in names:
+ w = want_psks.get(name, {})
+ h = have_psks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_psks and name not in want_psks:
+ self.commands.append("delete vpn ipsec authentication psk {0}".format(name))
+ continue
+
+ if name not in have_psks:
+ self.addcmd({"name": name}, "authentication.psk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.psk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.psk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", True)
+
+ w_dhcp = set(w.get("dhcp_interface") or [])
+ h_dhcp = set(h.get("dhcp_interface") or [])
+ for i in w_dhcp - h_dhcp:
+ self.addcmd(dict(ctx, dhcp_interface=i), "authentication.psk.dhcp_interface", False)
+ if reconcile:
+ for i in h_dhcp - w_dhcp:
+ self.addcmd(
+ dict(ctx, dhcp_interface=i),
+ "authentication.psk.dhcp_interface",
+ True,
+ )
+
+ def _compare_ppks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_ppks = haved.get("authentication", {}).get("ppk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("ppk", {})):
+ if name in have_ppks:
+ self.commands.append(
+ "delete vpn ipsec authentication ppk {0}".format(name),
+ )
+ return
+
+ want_ppks = wantd.get("authentication", {}).get("ppk", {})
+ names = set(want_ppks) | set(have_ppks) if select_all else set(want_ppks)
+
+ for name in names:
+ w = want_ppks.get(name, {})
+ h = have_ppks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_ppks and name not in want_ppks:
+ self.commands.append("delete vpn ipsec authentication ppk {0}".format(name))
+ continue
+
+ if name not in have_ppks:
+ self.addcmd({"name": name}, "authentication.ppk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.ppk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.ppk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", True)
+
+ # -------------------------------------------------------------------
+ # Top-level scalar / list / bool fields
+ #
+ # NOTE: these are all direct fields of the single top-level config
+ # object, not named collections -- there is no "item entirely absent
+ # from want" concept here, only "field omitted from want". So only
+ # `reconcile` applies; `select_all` is irrelevant at this level (it's
+ # accepted for a consistent call signature but unused).
+ # -------------------------------------------------------------------
+
+ def _compare_top_level(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ if scoped_delete is not None:
+ # Principle: naming a parameter under scoped `deleted` means
+ # "delete this specific value" -- a scalar/bool key present
+ # (regardless of value) signals whole-field removal; a list
+ # value present means "delete exactly these elements", not
+ # the whole list, mirroring vyos_vrf's bind_to_all precedent
+ # extended consistently to list- and nested-dict-shaped
+ # fields.
+ if "disable_uniqreqids" in scoped_delete and haved.get("disable_uniqreqids"):
+ self.commands.append("delete vpn ipsec disable-uniqreqids")
+
+ h_ifaces = set(haved.get("interface") or [])
+ for i in set(scoped_delete.get("interface") or []) & h_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ s_log = scoped_delete.get("log", {})
+ h_log = haved.get("log", {})
+ if "level" in s_log and "level" in h_log:
+ self.addcmd({"level": h_log["level"]}, "log.level", True)
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in set(s_log.get("subsystem") or []) & h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ s_opt = scoped_delete.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ if field in s_opt and h_opt.get(field):
+ self.addcmd({}, "options.{0}".format(field), True)
+ if "interface" in s_opt and "interface" in h_opt:
+ self.addcmd({"interface": h_opt["interface"]}, "options.interface", True)
+
+ s_retrans = s_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ if field in s_retrans and field in h_retrans:
+ self.addcmd(
+ {field: h_retrans[field]},
+ "options.retransmission.{0}".format(field),
+ True,
+ )
+ return
+
+ self._cmp_bool(wantd, haved, "disable_uniqreqids", {}, "disable_uniqreqids", reconcile)
+
+ w_ifaces = set(wantd.get("interface") or [])
+ h_ifaces = set(haved.get("interface") or [])
+ for i in w_ifaces - h_ifaces:
+ self.addcmd({"interface": i}, "interface", False)
+ if reconcile:
+ for i in h_ifaces - w_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ w_log = wantd.get("log", {})
+ h_log = haved.get("log", {})
+ self._cmp_scalar(w_log, h_log, "level", {}, "log.level", reconcile)
+ w_sub = set(w_log.get("subsystem") or [])
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in w_sub - h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", False)
+ if reconcile:
+ for s in h_sub - w_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ w_opt = wantd.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ self._cmp_bool(w_opt, h_opt, field, {}, "options.{0}".format(field), reconcile)
+ self._cmp_scalar(w_opt, h_opt, "interface", {}, "options.interface", reconcile)
+
+ w_retrans = w_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ self._cmp_scalar(
+ w_retrans,
+ h_retrans,
+ field,
+ {},
+ "options.retransmission.{0}".format(field),
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Field-level helpers (mirrors vyos_nat's _cmp_scalar / _cmp_bool)
+ # -------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif reconcile and h is not None:
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False):
+ # An explicitly-specified value (even False) is always enforced,
+ # regardless of state -- that's the user directly saying what
+ # they want. An OMITTED field is only enforced (i.e. deleted if
+ # currently True) under full reconciliation (replaced/overridden).
+ # Under merged, an omitted field is left alone -- protected
+ # further upstream by dict_merge backfilling `want` from `have`
+ # before this is ever reached, but this still needs to be correct
+ # in isolation (e.g. for a field nested inside a dict that wasn't
+ # part of the dict_merge'd top-level structure).
+ explicit = field in want
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h and (w or explicit or reconcile):
+ self.addcmd(dict(ctx), parser, not w)
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..65232ed7
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,355 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_vpn_ipsec_s2s config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+
+Mirrors vyos_vpn_ipsec's config.py exactly -- same list-to-dict
+conversion + explicit per-state branching, same select_all/reconcile
+two-flag design for the replaced/overridden distinction, same scoped
+deleted handling. See that file's own docstring for the full state
+semantics; the summary:
+
+ - merged: only items/fields named in `want` are touched.
+ - replaced: only items NAMED in `want` are touched, but each named
+ item is fully reconciled (omitted fields removed).
+ - overridden: every item is in scope, including ones absent from
+ `want` -- those get deleted wholesale. Named items
+ reconciled the same way as replaced.
+ - deleted: bare (no config) deletes everything; a scoped config
+ deletes only what's named, down to individual list
+ elements.
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
+ Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sTemplate,
+)
+
+
+class Vpn_ipsec_s2s(ResourceModule):
+ """
+ The vyos_vpn_ipsec_s2s config class
+ """
+
+ def __init__(self, module):
+ super(Vpn_ipsec_s2s, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vpn_ipsec_s2s",
+ tmplt=Vpn_ipsec_s2sTemplate(),
+ )
+
+ def execute_module(self):
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ wantd = deepcopy(self.want) or {}
+ haved = deepcopy(self.have) or {}
+
+ for entry in (wantd, haved):
+ self._list_to_dict(entry)
+
+ scoped_delete = None
+ if self.state == "deleted":
+ if wantd:
+ scoped_delete = wantd
+ wantd = {}
+
+ if self.state == "merged":
+ # list_to_dict() above must run BEFORE this -- dict_merge
+ # concatenates raw lists rather than merging matching items
+ # by key, so it only does the right thing once both sides
+ # are already name-keyed dicts.
+ wantd = dict_merge(haved, wantd)
+
+ select_all = self.state in ("overridden", "deleted")
+ reconcile = self.state in ("replaced", "overridden", "deleted")
+
+ self._compare_peers(wantd, haved, select_all, reconcile, scoped_delete)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------
+ # List -> name-keyed dict conversion
+ # -------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ peers = config.get("peer")
+ if isinstance(peers, list):
+ config["peer"] = {p["name"]: p for p in peers}
+ for peer in config["peer"].values():
+ if isinstance(peer.get("tunnel"), list):
+ peer["tunnel"] = {t["tunnel_id"]: t for t in peer["tunnel"]}
+
+ # -------------------------------------------------------------------
+ # Peers
+ # -------------------------------------------------------------------
+
+ def _compare_peers(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_peers = haved.get("peer", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("peer", {})):
+ if name in have_peers:
+ self.commands.append(
+ "delete vpn ipsec site-to-site peer {0}".format(name),
+ )
+ return
+
+ want_peers = wantd.get("peer", {})
+ names = set(want_peers) | set(have_peers) if select_all else set(want_peers)
+
+ for name in names:
+ w = want_peers.get(name, {})
+ h = have_peers.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_peers and name not in want_peers:
+ self.commands.append(
+ "delete vpn ipsec site-to-site peer {0}".format(name),
+ )
+ continue
+
+ if name not in have_peers:
+ self.addcmd({"name": name}, "peer", False)
+
+ ctx = {"name": name}
+ self._cmp_bool(w, h, "disable", ctx, "peer.disable", reconcile)
+
+ w_auth = w.get("authentication", {})
+ h_auth = h.get("authentication", {})
+ for field in ("local_id", "remote_id", "mode"):
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ field,
+ ctx,
+ "peer.authentication.{0}".format(field),
+ reconcile,
+ )
+ self._cmp_bool(
+ w_auth,
+ h_auth,
+ "use_x509_id",
+ ctx,
+ "peer.authentication.use_x509_id",
+ reconcile,
+ )
+
+ w_ppk = w_auth.get("ppk", {})
+ h_ppk = h_auth.get("ppk", {})
+ self._cmp_scalar(w_ppk, h_ppk, "id", ctx, "peer.authentication.ppk.id", reconcile)
+ self._cmp_bool(
+ w_ppk,
+ h_ppk,
+ "required",
+ ctx,
+ "peer.authentication.ppk.required",
+ reconcile,
+ )
+
+ w_rsa = w_auth.get("rsa", {})
+ h_rsa = h_auth.get("rsa", {})
+ for field in ("local_key", "remote_key", "passphrase"):
+ self._cmp_scalar(
+ w_rsa,
+ h_rsa,
+ field,
+ ctx,
+ "peer.authentication.rsa.{0}".format(field),
+ reconcile,
+ )
+
+ w_x509 = w_auth.get("x509", {})
+ h_x509 = h_auth.get("x509", {})
+ for field in ("certificate", "passphrase"):
+ self._cmp_scalar(
+ w_x509,
+ h_x509,
+ field,
+ ctx,
+ "peer.authentication.x509.{0}".format(field),
+ reconcile,
+ )
+ w_ca = set(w_x509.get("ca_certificate") or [])
+ h_ca = set(h_x509.get("ca_certificate") or [])
+ for cert in w_ca - h_ca:
+ self.addcmd(
+ dict(ctx, ca_certificate=cert),
+ "peer.authentication.x509.ca_certificate",
+ False,
+ )
+ if reconcile:
+ for cert in h_ca - w_ca:
+ self.addcmd(
+ dict(ctx, ca_certificate=cert),
+ "peer.authentication.x509.ca_certificate",
+ True,
+ )
+
+ for field in (
+ "childless",
+ "connection_type",
+ "default_esp_group",
+ "description",
+ "dhcp_interface",
+ "ike_group",
+ "ikev2_reauth",
+ "local_address",
+ ):
+ self._cmp_scalar(w, h, field, ctx, "peer.{0}".format(field), reconcile)
+ self._cmp_bool(
+ w,
+ h,
+ "force_udp_encapsulation",
+ ctx,
+ "peer.force_udp_encapsulation",
+ reconcile,
+ )
+ self._cmp_scalar(w, h, "replay_window", ctx, "peer.replay_window", reconcile)
+
+ w_remote_addr = set(w.get("remote_address") or [])
+ h_remote_addr = set(h.get("remote_address") or [])
+ for addr in w_remote_addr - h_remote_addr:
+ self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", False)
+ if reconcile:
+ for addr in h_remote_addr - w_remote_addr:
+ self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", True)
+
+ w_virt_addr = set(w.get("virtual_address") or [])
+ h_virt_addr = set(h.get("virtual_address") or [])
+ for addr in w_virt_addr - h_virt_addr:
+ self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", False)
+ if reconcile:
+ for addr in h_virt_addr - w_virt_addr:
+ self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", True)
+
+ self._compare_tunnels(w.get("tunnel", {}), h.get("tunnel", {}), ctx, reconcile)
+ self._compare_vti(w.get("vti", {}), h.get("vti", {}), ctx, reconcile)
+
+ # -------------------------------------------------------------------
+ # Tunnels (nested collection within a peer)
+ # -------------------------------------------------------------------
+
+ def _compare_tunnels(self, want_tunnels, have_tunnels, peer_ctx, reconcile):
+ # A tunnel collection lives entirely inside an already-selected
+ # peer -- once that peer is in scope, its own tunnels always get
+ # full reconciliation under replaced/overridden, matching how
+ # esp_group/ike_group's own nested proposals behave in the
+ # profile module.
+ ids = set(want_tunnels) | set(have_tunnels) if reconcile else set(want_tunnels)
+ for tid in ids:
+ w = want_tunnels.get(tid, {})
+ h = have_tunnels.get(tid, {})
+ if w == h:
+ continue
+
+ if tid in have_tunnels and tid not in want_tunnels:
+ self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", True)
+ continue
+
+ if tid not in have_tunnels:
+ self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", False)
+
+ ctx = dict(peer_ctx, tunnel_id=tid)
+ self._cmp_bool(w, h, "disable", ctx, "peer.tunnel.disable", reconcile)
+ for field in ("esp_group", "protocol"):
+ self._cmp_scalar(w, h, field, ctx, "peer.tunnel.{0}".format(field), reconcile)
+ self._cmp_scalar(w, h, "priority", ctx, "peer.tunnel.priority", reconcile)
+
+ for side in ("local", "remote"):
+ w_side = w.get(side, {})
+ h_side = h.get(side, {})
+ self._cmp_scalar(
+ w_side,
+ h_side,
+ "port",
+ ctx,
+ "peer.tunnel.{0}.port".format(side),
+ reconcile,
+ )
+ w_prefix = set(w_side.get("prefix") or [])
+ h_prefix = set(h_side.get("prefix") or [])
+ for p in w_prefix - h_prefix:
+ self.addcmd(
+ dict(ctx, prefix=p),
+ "peer.tunnel.{0}.prefix".format(side),
+ False,
+ )
+ if reconcile:
+ for p in h_prefix - w_prefix:
+ self.addcmd(
+ dict(ctx, prefix=p),
+ "peer.tunnel.{0}.prefix".format(side),
+ True,
+ )
+
+ # -------------------------------------------------------------------
+ # VTI (nested dict within a peer, not a collection)
+ # -------------------------------------------------------------------
+
+ def _compare_vti(self, w_vti, h_vti, peer_ctx, reconcile):
+ for field in ("bind", "esp_group"):
+ self._cmp_scalar(w_vti, h_vti, field, peer_ctx, "peer.vti.{0}".format(field), reconcile)
+
+ w_ts = w_vti.get("traffic_selector", {})
+ h_ts = h_vti.get("traffic_selector", {})
+ for side in ("local", "remote"):
+ w_prefix = set(w_ts.get(side, {}).get("prefix") or [])
+ h_prefix = set(h_ts.get(side, {}).get("prefix") or [])
+ parser = "peer.vti.traffic_selector.{0}.prefix".format(side)
+ for p in w_prefix - h_prefix:
+ self.addcmd(dict(peer_ctx, prefix=p), parser, False)
+ if reconcile:
+ for p in h_prefix - w_prefix:
+ self.addcmd(dict(peer_ctx, prefix=p), parser, True)
+
+ # -------------------------------------------------------------------
+ # Field-level helpers (mirrors vyos_vpn_ipsec's own)
+ # -------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif reconcile and h is not None:
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False):
+ explicit = field in want
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h and (w or explicit or reconcile):
+ self.addcmd(dict(ctx), parser, not w)
diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.py b/plugins/module_utils/network/vyos/config/vrf/vrf.py
index aa4329d9..a55073b0 100644
--- a/plugins/module_utils/network/vyos/config/vrf/vrf.py
+++ b/plugins/module_utils/network/vyos/config/vrf/vrf.py
@@ -7,7 +7,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py
index 31839c5d..dab92612 100644
--- a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py
+++ b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py
index 2883cc2d..cf2e4475 100644
--- a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py
+++ b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py
index 24c444dd..92c0f97a 100644
--- a/plugins/module_utils/network/vyos/facts/facts.py
+++ b/plugins/module_utils/network/vyos/facts/facts.py
@@ -6,8 +6,8 @@ The facts class for vyos
this file validates each subset of facts and selectively
calls the appropriate facts gathering function
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -30,6 +30,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firew
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import (
Firewall_rulesFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ha.ha import (
+ HaFacts,
+)
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.hostname.hostname import (
HostnameFacts,
)
@@ -56,6 +59,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import (
Logging_globalFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat import (
+ NatFacts,
+)
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import (
Ntp_globalFacts,
)
@@ -80,9 +86,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import (
Static_routesFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecFacts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sFacts,
+)
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts
-
FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config)
FACT_RESOURCE_SUBSETS = dict(
interfaces=InterfacesFacts,
@@ -94,6 +105,7 @@ FACT_RESOURCE_SUBSETS = dict(
firewall_rules=Firewall_rulesFacts,
firewall_global=Firewall_globalFacts,
firewall_interfaces=Firewall_interfacesFacts,
+ ha=HaFacts,
ospfv3=Ospfv3Facts,
ospfv2=Ospfv2Facts,
ospf_interfaces=Ospf_interfacesFacts,
@@ -103,9 +115,12 @@ FACT_RESOURCE_SUBSETS = dict(
prefix_lists=Prefix_listsFacts,
logging_global=Logging_globalFacts,
ntp_global=Ntp_globalFacts,
+ nat=NatFacts,
snmp_server=Snmp_serverFacts,
hostname=HostnameFacts,
vrf=VrfFacts,
+ vpn_ipsec=Vpn_ipsecFacts,
+ vpn_ipsec_s2s=Vpn_ipsec_s2sFacts,
)
diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
index 3a48889e..e13c1939 100644
--- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
+++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -77,13 +77,17 @@ class Firewall_globalFacts(object):
:rtype: dictionary
:returns: The generated config
"""
+
conf = "\n".join(
filter(
- lambda x: ("firewall ipv6-name" and "firewall name" not in x),
+ lambda x: not (
+ x.startswith("set firewall name")
+ or x.startswith("set firewall ipv6-name")
+ or x.startswith("set firewall ipv6 name")
+ ),
conf,
),
)
-
a_lst = [
"config_trap",
"validation",
@@ -97,6 +101,7 @@ class Firewall_globalFacts(object):
"group": self.parse_group(conf),
"route_redirects": self.route_redirects(conf),
"state_policy": self.parse_state_policy(conf),
+ "zone": self.parse_zone(conf),
}
firewall.update(f_sub)
return firewall
@@ -400,3 +405,120 @@ class Firewall_globalFacts(object):
"twa_hazards_protection",
)
return True if attrib in bool_set else False
+
+ def parse_zone(self, conf):
+ """
+ This function triggers the parsing of 'zone' attributes.
+ :param conf: configuration.
+ :return: generated config dictionary.
+ """
+ cfg_dict = {}
+
+ KEY_MAP = {
+ "interface": "interfaces",
+ "intra-zone-filtering": "intra-zone-filtering",
+ "from": "sources",
+ }
+
+ LIST_ATTRS = {
+ "interfaces",
+ "intra_zone_filtering",
+ "sources",
+ }
+
+ for line in conf.splitlines():
+
+ m = search(
+ r"^set firewall zone (?P<zone>\S+)\s+(?P<attr>[a-z-]+)(?:\s+(?P<value>'[^']+'|[^\n]+))?$",
+ line,
+ )
+ if not m:
+ continue
+
+ zone_name = m.group("zone")
+ raw_attr = m.group("attr").replace("-", "_")
+ value = m.group("value")
+
+ if value is None:
+ value = True
+ else:
+ value = value.strip("'")
+
+ # VyOS 1.5.0 GA wraps 'interface' under a new 'member' node:
+ # "set firewall zone <name> member interface <ifname>". Unwrap
+ # it here so it lands in the same 'interfaces' list as the
+ # pre-1.5.0 bare "interface <ifname>" form. No version check
+ # needed -- 1.4.x/1.5-rolling configs never emit 'member'.
+ if raw_attr == "member" and isinstance(value, str) and value.startswith("interface "):
+ raw_attr = "interface"
+ value = value.split(None, 1)[1].strip("'")
+
+ zone = cfg_dict.setdefault(zone_name, {"name": zone_name})
+
+ attr = KEY_MAP.get(raw_attr, raw_attr)
+
+ if attr in LIST_ATTRS:
+ if attr == "intra_zone_filtering":
+ izf = zone.setdefault(attr, {})
+ izf_attr = self._parse_izf(value)
+ for k, v in izf_attr.items():
+ if isinstance(v, dict):
+ izf.setdefault(k, {}).update(v)
+ else:
+ izf[k] = v
+ elif attr == "sources":
+ self._parse_sources(zone, value)
+ else:
+ zone.setdefault(attr, []).append(value)
+ else:
+ zone[attr] = value
+
+ return list(cfg_dict.values())
+
+ def _parse_izf(self, value):
+
+ tokens = value.replace("'", "").split()
+
+ result = {}
+
+ key = tokens[0].replace("-", "_")
+
+ if len(tokens) == 2:
+ result[key] = tokens[1]
+
+ elif len(tokens) >= 3:
+ subkey = tokens[1].replace("-", "_")
+ result[key] = {subkey: tokens[2]}
+
+ return result
+
+ def _parse_sources(self, zone, value):
+
+ tokens = value.split()
+
+ if len(tokens) < 1:
+ return
+
+ src_zone = tokens[0]
+
+ sources = zone.setdefault("sources", [])
+
+ entry = None
+ for s in sources:
+ if s.get("zone") == src_zone:
+ entry = s
+ break
+
+ if entry is None:
+ entry = {"zone": src_zone}
+ sources.append(entry)
+
+ if len(tokens) == 1:
+ return
+
+ if tokens[1] == "firewall" and len(tokens) >= 4:
+ key = tokens[2].replace("-", "_")
+ val = tokens[3].strip("'")
+
+ firewall = entry.setdefault("firewall", {})
+ firewall[key] = val
diff --git a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py
index bac31920..34235af7 100644
--- a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py
index f8f02e7a..31cc1fa5 100644
--- a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py
+++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
@@ -538,7 +538,7 @@ class Firewall_rulesFacts(object):
if out:
val = out.group(1).strip("'")
if "/" in val: # number/unit
- (number, unit) = val.split("/")
+ number, unit = val.split("/")
config["number"] = number
config["unit"] = unit
return config
diff --git a/plugins/module_utils/network/vyos/facts/ha/__init__.py b/plugins/module_utils/network/vyos/facts/ha/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/ha/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py
new file mode 100644
index 00000000..fc559783
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/ha/ha.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_ha fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ha.ha import (
+ HaArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import (
+ HaTemplate,
+)
+
+
+class HaFacts(object):
+ """The vyos_ha facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = HaArgs.argument_spec
+
+ def get_config(self, connection):
+ return connection.get('show configuration commands | match "set high-availability"')
+
+ def get_config_set(self, data, connection):
+ """Classify config lines into per-object buckets for isolated parsing.
+
+ Each bucket is parsed by a single HaTemplate instance so that facts
+ from different objects (groups, sync-groups, virtual-servers) never
+ bleed into each other.
+
+ Keys are namespaced to avoid collisions between a VRRP group and a
+ sync-group that share the same name (e.g. both named "g1").
+ An elif chain ensures each line lands in exactly one bucket.
+ """
+ config_dict = {}
+ for config_line in data.splitlines():
+ vrrp_disable = re.search(r"set high-availability disable", config_line)
+ vrrp_snmp = re.search(r"set high-availability vrrp snmp", config_line)
+ vrrp_gp = re.search(
+ r"set high-availability vrrp global-parameters (\S+).*",
+ config_line,
+ )
+ vrrp_grp = re.search(r"set high-availability vrrp group (\S+).*", config_line)
+ vrrp_sg = re.search(r"set high-availability vrrp sync-group (\S+).*", config_line)
+ vrrp_vsrv = re.search(r"set high-availability virtual-server (\S+).*", config_line)
+
+ if vrrp_disable:
+ config_dict.setdefault("disable", []).append(config_line)
+ elif vrrp_snmp:
+ config_dict.setdefault("vrrp", []).append(config_line)
+ elif vrrp_gp:
+ config_dict.setdefault("global_parameters", []).append(config_line)
+ elif vrrp_grp:
+ key = "vrrp_group_{0}".format(vrrp_grp.group(1))
+ config_dict.setdefault(key, []).append(config_line)
+ elif vrrp_sg:
+ key = "vrrp_sg_{0}".format(vrrp_sg.group(1))
+ config_dict.setdefault(key, []).append(config_line)
+ elif vrrp_vsrv:
+ config_dict.setdefault(vrrp_vsrv.group(1), []).append(config_line)
+
+ return list(config_dict.values())
+
+ def deep_merge(self, dest, src):
+ for key, value in src.items():
+ if key in dest and isinstance(dest[key], dict) and isinstance(value, dict):
+ self.deep_merge(dest[key], value)
+ else:
+ dest[key] = value
+ return dest
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for vrrp network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+ objs = {}
+
+ if not data:
+ data = self.get_config(connection)
+ resources = self.get_config_set(data, connection)
+ vrrp_facts = {"disable": False, "virtual_servers": {}, "vrrp": {}}
+ for resource in resources:
+ vrrp_parser = HaTemplate(
+ lines=resource,
+ module=self._module,
+ )
+ objs = vrrp_parser.parse()
+ if "disable" in objs:
+ vrrp_facts["disable"] = objs["disable"]
+
+ for section in ("virtual_servers", "vrrp"):
+ if section in objs:
+ for name, data in objs[section].items():
+ if not isinstance(data, dict):
+ vrrp_facts[section][name] = data
+ continue
+ existing = vrrp_facts[section].get(name, {})
+ vrrp_facts[section][name] = self.deep_merge(existing, data)
+
+ ansible_facts["ansible_network_resources"].pop("ha", None)
+
+ vrrp_facts = self.normalize_config(vrrp_facts)
+
+ validate_parser = HaTemplate(lines=[], module=self._module)
+ params = utils.remove_empties(
+ validate_parser.validate_config(
+ self.argument_spec,
+ {"config": vrrp_facts},
+ redact=True,
+ ),
+ )
+
+ facts["ha"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+ return ansible_facts
+
+ def normalize_config(self, config):
+ if not config:
+ return config
+
+ if isinstance(config.get("virtual_servers"), dict):
+ config["virtual_servers"] = list(config["virtual_servers"].values())
+
+ vrrp = config.get("vrrp", {})
+
+ if isinstance(vrrp.get("groups"), dict):
+ vrrp["groups"] = list(vrrp["groups"].values())
+
+ if isinstance(vrrp.get("sync_groups"), dict):
+ vrrp["sync_groups"] = list(vrrp["sync_groups"].values())
+
+ # Normalize real_server inside each virtual_server
+ for vs in config.get("virtual_servers", []):
+ if isinstance(vs.get("real_server"), dict):
+ vs["real_server"] = list(vs["real_server"].values())
+
+ for group in vrrp.get("groups", []):
+ if isinstance(group.get("address"), list):
+ group["address"] = sorted(group["address"])
+
+ if isinstance(group.get("excluded_address"), list):
+ group["excluded_address"] = sorted(group["excluded_address"])
+
+ if isinstance(group.get("track", {}).get("interface"), list):
+ group["track"]["interface"] = sorted(group["track"]["interface"])
+
+ for sg in vrrp.get("sync_groups", []):
+ if isinstance(sg.get("member"), list):
+ sg["member"] = sorted(sg["member"])
+
+ return config
diff --git a/plugins/module_utils/network/vyos/facts/hostname/hostname.py b/plugins/module_utils/network/vyos/facts/hostname/hostname.py
index b4f7c529..18a66357 100644
--- a/plugins/module_utils/network/vyos/facts/hostname/hostname.py
+++ b/plugins/module_utils/network/vyos/facts/hostname/hostname.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py
index cd8008c6..30d619a3 100644
--- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py
@@ -12,7 +12,6 @@ based on the configuration.
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
@@ -80,6 +79,7 @@ class InterfacesFacts(object):
facts["interfaces"].append(utils.remove_empties(cfg))
ansible_facts["ansible_network_resources"].update(facts)
+
return ansible_facts
def render_config(self, conf):
@@ -94,7 +94,7 @@ class InterfacesFacts(object):
"""
vif_conf = "\n".join(filter(lambda x: ("vif" in x), conf))
eth_conf = "\n".join(filter(lambda x: ("vif" not in x), conf))
- config = self.parse_attribs(["description", "speed", "mtu", "duplex"], eth_conf)
+ config = self.parse_attribs(["description", "speed", "mtu", "duplex", "vrf"], eth_conf)
config["vifs"] = self.parse_vifs(vif_conf)
return utils.remove_empties(config)
diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py
index d62ea9b8..9e79e24d 100644
--- a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py
@@ -12,7 +12,6 @@ based on the configuration.
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py
index 8e1c8624..94c73416 100644
--- a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/legacy/base.py b/plugins/module_utils/network/vyos/facts/legacy/base.py
index 30978e0e..59666e19 100644
--- a/plugins/module_utils/network/vyos/facts/legacy/base.py
+++ b/plugins/module_utils/network/vyos/facts/legacy/base.py
@@ -11,7 +11,6 @@ based on the configuration.
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
import platform
import re
diff --git a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py
index 7a6e9b8e..40ff2ad3 100644
--- a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py
+++ b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py
index e029b47a..72bdf154 100644
--- a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py
@@ -12,7 +12,6 @@ based on the configuration.
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py
index da80d967..243043bc 100644
--- a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py
+++ b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
@@ -23,6 +22,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.log
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import (
Logging_globalTemplate,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import (
+ Logging_globalTemplate15,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import (
+ LooseVersion,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version
class Logging_globalFacts(object):
@@ -80,8 +86,17 @@ class Logging_globalFacts(object):
if not data:
data = self.get_logging_data(connection)
- # parse native config using the Logging_global template
- logging_global_parser = Logging_globalTemplate(lines=data.splitlines(), module=self._module)
+ if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"):
+ logging_global_parser = Logging_globalTemplate15(
+ lines=data.splitlines(),
+ module=self._module,
+ )
+ else:
+ logging_global_parser = Logging_globalTemplate(
+ lines=data.splitlines(),
+ module=self._module,
+ )
+
objs = logging_global_parser.parse()
ansible_facts["ansible_network_resources"].pop("logging_global", None)
objs = self.process_facts(objs)
diff --git a/plugins/module_utils/network/vyos/facts/nat/__init__.py b/plugins/module_utils/network/vyos/facts/nat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/nat/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py
new file mode 100644
index 00000000..73d6238f
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/nat/nat.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import (
+ NatArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import (
+ NatTemplate,
+)
+
+
+class NatFacts(object):
+ """The vyos nat facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = NatArgs.argument_spec
+
+ def get_config(self, connection):
+ return connection.get("show configuration commands | match 'set nat'")
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ facts = {}
+ config_lines = []
+
+ if not data:
+ data = self.get_config(connection)
+
+ for resource in data.splitlines():
+ config_lines.append(re.sub(r"'([^']*)'", r"\1", resource))
+
+ nat_parser = NatTemplate(lines=config_lines, module=self._module)
+ objs = nat_parser.parse()
+ objs = self._normalise(objs)
+
+ ansible_facts["ansible_network_resources"].pop("nat", None)
+
+ params = utils.remove_empties(
+ nat_parser.validate_config(self.argument_spec, {"config": objs}, redact=True),
+ )
+
+ if params.get("config"):
+ facts["nat"] = params["config"]
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
+
+ def _deep_merge(self, base, override):
+ for k, v in override.items():
+ if k in base and isinstance(base[k], dict) and isinstance(v, dict):
+ self._deep_merge(base[k], v)
+ elif k in base and isinstance(base[k], list) and isinstance(v, list):
+ for entry in v:
+ if entry not in base[k]:
+ base[k].append(entry)
+ else:
+ base[k] = v
+ return base
+
+ def _merge_rule_list(self, rules):
+ merged = {}
+ for item in rules:
+ rid = item["id"]
+ if rid not in merged:
+ merged[rid] = {"id": rid}
+ for k, v in item.items():
+ if k == "id":
+ continue
+ if isinstance(v, list):
+ existing = merged[rid].setdefault(k, [])
+ for entry in v:
+ if entry not in existing:
+ existing.append(entry)
+ elif isinstance(v, dict):
+ merged[rid].setdefault(k, {})
+ self._deep_merge(merged[rid][k], v)
+ else:
+ merged[rid][k] = v
+ return list(merged.values())
+
+ def _merge_pool_list(self, pools):
+ merged = {}
+ for item in pools:
+ name = item["name"]
+ if name not in merged:
+ merged[name] = {"name": name}
+ for k, v in item.items():
+ if k == "name":
+ continue
+ if k == "range" and isinstance(v, list):
+ existing = merged[name].setdefault(k, [])
+ existing.extend(v)
+ if v and isinstance(v[0], dict):
+ merged[name][k] = self._merge_range_list(existing)
+ else:
+ merged[name][k] = list(dict.fromkeys(existing))
+ elif isinstance(v, list):
+ merged[name].setdefault(k, [])
+ for val in v:
+ if val not in merged[name][k]:
+ merged[name][k].append(val)
+ elif isinstance(v, dict):
+ merged[name].setdefault(k, {})
+ self._deep_merge(merged[name][k], v)
+ else:
+ merged[name][k] = v
+ return list(merged.values())
+
+ def _merge_range_list(self, ranges):
+ """Merge external pool range entries by value, preserving seq."""
+ merged = {}
+ for entry in ranges:
+ if isinstance(entry, dict):
+ key = entry.get("value") or entry.get("address", "")
+ if not key:
+ continue
+ if key not in merged:
+ merged[key] = {"value": key}
+ if entry.get("seq"):
+ merged[key]["seq"] = entry["seq"]
+ else:
+ if entry not in merged:
+ merged[entry] = {"value": entry}
+ return list(merged.values())
+
+ def _normalise(self, objs):
+ for nat_type in ["nat", "nat64", "nat66"]:
+ nat = objs.get(nat_type)
+ if not nat:
+ continue
+
+ for section in ["destination", "source", "static", "cgnat"]:
+ if section not in nat:
+ continue
+ rules = nat[section].get("rule")
+ if isinstance(rules, list):
+ nat[section]["rule"] = self._merge_rule_list(rules)
+ nat[section]["rule"].sort(key=lambda x: x.get("id", 0))
+
+ if "cgnat" in nat and "pool" in nat["cgnat"]:
+ pool = nat["cgnat"]["pool"]
+ for ptype in ["external", "internal"]:
+ if ptype in pool and isinstance(pool[ptype], list):
+ pool[ptype] = self._merge_pool_list(pool[ptype])
+
+ if nat_type == "nat64":
+ for rule in nat.get("source", {}).get("rule", []):
+ pools = rule.get("translation", {}).get("pool")
+ if pools and isinstance(pools, list):
+ rule["translation"]["pool"] = self._merge_rule_list(pools)
+ rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0))
+
+ self._cast_ports(objs)
+ return objs
+
+ def _cast_ports(self, obj):
+ """Recursively cast known integer port/seq fields to str."""
+ if isinstance(obj, dict):
+ for k, v in obj.items():
+ if k in ("port", "seq") and isinstance(v, int):
+ obj[k] = str(v)
+ else:
+ self._cast_ports(v)
+ elif isinstance(obj, list):
+ for item in obj:
+ self._cast_ports(item)
diff --git a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py
index 880bc79f..a6d6892f 100644
--- a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py
+++ b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py
index 2160fc71..1a9a687d 100644
--- a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py
+++ b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py
index d07bf13a..056f949d 100644
--- a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py
+++ b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py
index ae67a4b6..5fd9e627 100644
--- a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py
+++ b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py
@@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
-from __future__ import absolute_import, division, print_function
+from __future__ import absolute_import, division, print_function
__metaclass__ = type
diff --git a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py
index 17f63fb2..4bdcbf8c 100644
--- a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py
+++ b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py
index cfae26e3..d4084fd7 100644
--- a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py
+++ b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py
index d961a0ff..d3ff02e5 100644
--- a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py
+++ b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py
index 710d91fe..1bce772c 100644
--- a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py
+++ b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py
@@ -12,7 +12,6 @@ based on the configuration.
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from copy import deepcopy
from re import M, findall, search
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..695b9b8d
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha (explicit process_facts() naming each
+name-keyed dict that needs converting to a list), matching the config.py
+convention for this module, rather than a generic argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsecFacts(object):
+ """The vyos vpn_ipsec facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsecArgs.argument_spec
+
+ def get_vpn_ipsec_data(self, connection):
+ return connection.get('show configuration commands | match "vpn ipsec"')
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects. Each key handled explicitly, matching
+ the vyos_logging_global/vyos_ha convention.
+ """
+ if not objFinal:
+ return objFinal
+
+ for key in ("ike_group", "esp_group"):
+ if key in objFinal:
+ items = list(objFinal[key].values())
+ for item in items:
+ if "proposal" in item:
+ item["proposal"] = sorted(
+ item["proposal"].values(),
+ key=lambda p: int(p["proposal_id"]),
+ )
+ objFinal[key] = sorted(items, key=lambda item: item["name"])
+
+ if "profile" in objFinal:
+ objFinal["profile"] = sorted(
+ objFinal["profile"].values(),
+ key=lambda item: item["name"],
+ )
+
+ if "authentication" in objFinal:
+ auth = objFinal["authentication"]
+ for key in ("psk", "ppk"):
+ if key in auth:
+ auth[key] = sorted(
+ auth[key].values(),
+ key=lambda item: item["name"],
+ )
+
+ return objFinal
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_data(connection)
+
+ vpn_ipsec_parser = Vpn_ipsecTemplate(lines=data.splitlines(), module=self._module)
+ objs = vpn_ipsec_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..6c9b5d8e
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,115 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec_s2s fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts()
+naming each name-keyed dict that needs converting to a list), matching
+the config.py convention for this module, rather than a generic
+argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sTemplate,
+)
+
+
+class Vpn_ipsec_s2sFacts(object):
+ """The vyos vpn_ipsec_s2s facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec
+
+ def get_vpn_ipsec_s2s_data(self, connection):
+ return connection.get(
+ 'show configuration commands | match "vpn ipsec site-to-site"',
+ )
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects.
+
+ NOTE: every PARSERS result template in rm_templates.py nests its
+ output under "site_to_site" -> "peer" (mirroring the CLI's own
+ tree: `vpn ipsec site-to-site peer <name> ...`), but the
+ argspec's `config` has `peer` directly at the top level -- there
+ is no `site_to_site` wrapper in the argspec, since that's the
+ one node wrap_docstring.py unwrapped when building the
+ docstring (its own children became config's children directly).
+ So this needs to strip that outer key, not just convert the
+ name-keyed dicts to lists.
+ """
+ if not objFinal:
+ return objFinal
+
+ site_to_site = objFinal.get("site_to_site", {})
+ peers = site_to_site.get("peer", {})
+
+ items = list(peers.values())
+ for item in items:
+ if "tunnel" in item:
+ item["tunnel"] = sorted(
+ item["tunnel"].values(),
+ key=lambda t: int(t["tunnel_id"]),
+ )
+
+ return {"peer": sorted(items, key=lambda item: item["name"])}
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec_s2s network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_s2s_data(connection)
+
+ vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate(
+ lines=data.splitlines(),
+ module=self._module,
+ )
+ objs = vpn_ipsec_s2s_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_s2s_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec_s2s"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/facts/vrf/vrf.py b/plugins/module_utils/network/vyos/facts/vrf/vrf.py
index 6c7b8918..74f46b45 100644
--- a/plugins/module_utils/network/vyos/facts/vrf/vrf.py
+++ b/plugins/module_utils/network/vyos/facts/vrf/vrf.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py
index fccfc9c2..fe7889aa 100644
--- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py
+++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py
index 09cd41a7..9936cf6d 100644
--- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py
+++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py
index 03f6265b..730fa5ee 100644
--- a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py
+++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py
index cbe21e3a..b86e233c 100644
--- a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py
+++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/ha.py b/plugins/module_utils/network/vyos/rm_templates/ha.py
new file mode 100644
index 00000000..300b14a7
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/ha.py
@@ -0,0 +1,1011 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The Ha parser templates file. This contains
+a list of parser definitions and associated functions that
+facilitates both facts gathering and native command generation for
+the given network resource.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+def _tmplt_vsrvs(config_data):
+ config_data = config_data["virtual_servers"]
+ command = []
+
+ cmd = "high-availability virtual-server {name}".format(**config_data)
+ for key, value in config_data.items():
+ if key == "name" or isinstance(value, dict) or value is None:
+ continue
+ else:
+ command.append(f"{cmd} {key.replace('_', '-')} {value}")
+
+ return command
+
+
+def _tmplt_vsrvs_rsrv(config_data):
+ config_data = config_data["virtual_servers"]
+ command = []
+ cmd = "high-availability virtual-server {name}".format(**config_data)
+ config_data = config_data["real_server"]
+ address = config_data["address"]
+ for key, value in config_data.items():
+ if key == "address" or value is None:
+ continue
+ if value is not None and key == "health_check_script":
+ command.append(cmd + " real-server " + address + " health-check script " + value)
+ else:
+ command.append(cmd + " real-server " + f"{address} {key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_sgroup_hc(config_data):
+ config_data = config_data["vrrp"]["sync_groups"]
+ command = []
+ cmd = "high-availability vrrp sync-group {name}".format(**config_data)
+ config_data = config_data["health_check"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_sgroup_ts(config_data):
+ config_data = config_data["vrrp"]["sync_groups"]
+ command = []
+ cmd = "high-availability vrrp sync-group {name}".format(**config_data)
+ config_data = config_data["transition_script"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_gp(config_data):
+ config_data = config_data["vrrp"]["global_parameters"]
+ command = []
+
+ cmd = "high-availability vrrp global-parameters".format(**config_data)
+ for key, value in config_data.items():
+ if isinstance(value, dict) or value is None:
+ continue
+ else:
+ command.append(f"{cmd} {key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_gp_garp(config_data):
+ config_data = config_data["vrrp"]["global_parameters"]["garp"]
+ command = []
+ cmd = "high-availability vrrp global-parameters garp"
+
+ for key, value in config_data.items():
+ if value is None:
+ continue
+ command.append(f"{cmd} {key.replace('_', '-')} {value}")
+
+ return command
+
+
+def _tmplt_vrrp_group(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+
+ for key, value in config_data.items():
+ if (
+ key == "name"
+ or isinstance(value, dict)
+ or isinstance(value, list)
+ or isinstance(value, bool)
+ or value is None
+ ):
+ continue
+ else:
+ if key == "description":
+ value = f"'{value}'"
+ command.append(f"{cmd} {key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_group_bool(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+
+ for key, value in config_data.items():
+ if key != "name" and value is not None:
+ command.append(f"{cmd} {key.replace('_', '-')}")
+ return command
+
+
+def _tmplt_vrrp_group_garp(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["garp"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " garp " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_group_auth(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["authentication"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " authentication " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_group_ts(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["transition_script"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_sgroup_member(config_data):
+ sgroup = config_data["vrrp"]["sync_groups"]
+ command = []
+ cmd = "high-availability vrrp sync-group {name}".format(**sgroup)
+ members = sgroup.get("member", [])
+ for member in members:
+ if member is None:
+ continue
+ command.append(f"{cmd} member {member}")
+ return command
+
+
+def _tmplt_vrrp_group_exaddress(config_data):
+ group = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**group)
+ exaddresses = group.get("excluded_address", [])
+ for exaddress in exaddresses:
+ if exaddress is None:
+ continue
+ command.append(f"{cmd} excluded-address {exaddress}")
+ return command
+
+
+def _tmplt_vrrp_group_address(config_data):
+ group = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**group)
+ addresses = group.get("address", [])
+ for address in addresses:
+ if address is None:
+ continue
+ command.append(f"{cmd} address {address}")
+ return command
+
+
+def _tmplt_vrrp_group_hc(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["health_check"]
+ for key, value in config_data.items():
+ if value is not None:
+ command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}")
+ return command
+
+
+def _tmplt_vrrp_group_track_list(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["track"]
+ for key, value in config_data.items():
+ if isinstance(value, list) and value is not None and key != "name":
+ for item in value:
+ command.append(cmd + " track " + f"{key.replace('_', '-')} {item}")
+ return command
+
+
+def _tmplt_vrrp_group_track_bool(config_data):
+ config_data = config_data["vrrp"]["groups"]
+ command = []
+ cmd = "high-availability vrrp group {name}".format(**config_data)
+ config_data = config_data["track"]
+ for key, value in config_data.items():
+ if key != "name" and value is not None:
+ command.append(cmd + " track " + f"{key.replace('_', '-')}")
+ return command
+
+
+class HaTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(HaTemplate, self).__init__(
+ lines=lines,
+ tmplt=self,
+ prefix=prefix,
+ module=module,
+ )
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \shigh-availability
+ \s(?P<disable>disable)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "high-availability disable",
+ "result": {
+ "disable": "{{ True if disable is defined else False }}",
+ },
+ },
+ {
+ "name": "virtual_servers.address",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+address\s+(?P<address>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "address": "{{ address if address is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.algorithm",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+algorithm\s+(?P<algorithm>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "algorithm": "{{ algorithm if algorithm is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.delay_loop",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+delay-loop\s+(?P<delay_loop>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "delay_loop": "{{ delay_loop if delay_loop is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.forward_method",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+forward-method\s+(?P<forward_method>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "forward_method": "{{ forward_method if forward_method is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.fwmark",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+fwmark\s+(?P<fwmark>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "fwmark": "{{ fwmark if fwmark is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.persistence_timeout",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+persistence-timeout\s+(?P<persistence_timeout>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "persistence_timeout": "{{ persistence_timeout if persistence_timeout is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.port",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+port\s+(?P<port>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "port": "{{ port if port is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.protocol",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ (?:\s+protocol\s+(?P<protocol>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "protocol": "{{ protocol if protocol is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.real_server.port",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ \sreal-server
+ \s+(?P<address>\S+)
+ (?:\s+port\s+(?P<port>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs_rsrv,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "real_server": {
+ "{{ address }}": {
+ "address": "{{ address }}",
+ "port": "{{ port if port is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.real_server.health_check_script",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ \sreal-server
+ \s+(?P<address>\S+)
+ (?:\s+health-check\sscript\s+(?P<hcscript>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs_rsrv,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "real_server": {
+ "{{ address }}": {
+ "address": "{{ address }}",
+ "health_check_script": "{{ hcscript if hcscript is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "virtual_servers.real_server.connection_timeout",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svirtual-server
+ \s+(?P<name>\S+)
+ \sreal-server
+ \s+(?P<address>\S+)
+ (?:\s+connection-timeout\s+(?P<cont>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vsrvs_rsrv,
+ "result": {
+ "virtual_servers": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "real_server": {
+ "{{ address }}": {
+ "address": "{{ address }}",
+ "connection_timeout": "{{ cont if cont is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.sync_groups.member",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\ssync-group
+ \s+(?P<sgname>\S+)
+ \smember
+ \s+(?P<member>\S+)
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_sgroup_member,
+ "result": {
+ "vrrp": {
+ "sync_groups": {
+ "{{ sgname }}": {
+ "name": "{{ sgname }}",
+ "member": [
+ "{{ member }}",
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.sync_groups.health_check",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\ssync-group
+ \s+(?P<sgname>\S+)
+ \shealth-check
+ (?:\s+failure-count\s+(?P<failure_count>\S+))?
+ (?:\s+interval\s+(?P<int>\S+))?
+ (?:\s+ping\s+(?P<ping>\S+))?
+ (?:\s+script\s+(?P<script>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_sgroup_hc,
+ "result": {
+ "vrrp": {
+ "sync_groups": {
+ "{{ sgname }}": {
+ "name": "{{ sgname }}",
+ "health_check": {
+ "failure_count": "{{ failure_count if failure_count is defined else None }}",
+ "interval": "{{ int if int is defined else None }}",
+ "ping": "{{ ping if ping is defined else None }}",
+ "script": "{{ script if script is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.sync_groups.transition_script",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\ssync-group
+ \s+(?P<sgname>\S+)
+ \stransition-script
+ (?:\s+backup\s+(?P<backup>\S+))?
+ (?:\s+fault\s+(?P<fault>\S+))?
+ (?:\s+master\s+(?P<master>\S+))?
+ (?:\s+stop\s+(?P<stop>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_sgroup_ts,
+ "result": {
+ "vrrp": {
+ "sync_groups": {
+ "{{ sgname }}": {
+ "name": "{{ sgname }}",
+ "transition_script": {
+ "backup": "{{ backup if backup is defined else None }}",
+ "fault": "{{ fault if fault is defined else None }}",
+ "master": "{{ master if master is defined else None }}",
+ "stop": "{{ stop if stop is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.global_parameters.garp",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sglobal-parameters
+ \s+garp
+ (?:\s+interval\s+(?P<interval>\S+))?
+ (?:\s+master-delay\s+(?P<master_delay>\S+))?
+ (?:\s+master-refresh\s+(?P<master_refresh>\S+))?
+ (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))?
+ (?:\s+master-repeat\s+(?P<master_repeat>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_gp_garp,
+ "result": {
+ "vrrp": {
+ "global_parameters": {
+ "garp": {
+ "interval": "{{ interval if interval is defined else None }}",
+ "master_delay": "{{ master_delay if master_delay is defined else None }}",
+ "master_refresh": "{{ master_refresh if master_refresh is defined else None }}",
+ "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}",
+ "master_repeat": "{{ master_repeat if master_repeat is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.global_parameters",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sglobal-parameters
+ (?=\s+(?:startup-delay|version)\s)
+ (?:\s+startup-delay\s+(?P<startup_delay>\S+))?
+ (?:\s+version\s+(?P<version>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_gp,
+ "result": {
+ "vrrp": {
+ "global_parameters": {
+ "startup_delay": "{{ startup_delay if startup_delay is defined else None }}",
+ "version": "{{ version if version is defined else None }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ (?:\s+description\s+(?P<description>'.+?'|\S+))?
+ (?:\s+advertise-interval\s+(?P<advertise_interval>\S+))?
+ (?:\s+hello-source-address\s+(?P<hello_source>\S+))?
+ (?:\s+interface\s+(?P<interface>\S+))?
+ (?:\s+peer-address\s+(?P<peer_address>\S+))?
+ (?:\s+preempt-delay\s+(?P<preempt_delay>\S+))?
+ (?:\s+priority\s+(?P<priority>\S+))?
+ (?:\s+vrid\s+(?P<vrid>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "description": "{{ description | replace(\"'\", \"\") if description is defined else None }}",
+ "advertise_interval": "{{ advertise_interval if advertise_interval is defined else None }}",
+ "hello_source_address": "{{ hello_source if hello_source is defined else None }}",
+ "interface": "{{ interface if interface is defined else None }}",
+ "peer_address": "{{ peer_address if peer_address is defined else None }}",
+ "preempt_delay": "{{ preempt_delay if preempt_delay is defined else None }}",
+ "priority": "{{ priority if priority is defined else None }}",
+ "vrid": "{{ vrid if vrid is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.excluded_address",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \sexcluded-address
+ \s+(?P<excluded_address>.*)
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_exaddress,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "excluded_address": [
+ "{{ excluded_address | replace(\"'\", \"\") }}",
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.address",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \saddress
+ \s+(?P<address>.*)
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_address,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "address": [
+ "{{ address | replace(\"'\", \"\") }}",
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.garp",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \s+garp
+ (?:\s+interval\s+(?P<interval>\S+))?
+ (?:\s+master-delay\s+(?P<master_delay>\S+))?
+ (?:\s+master-refresh\s+(?P<master_refresh>\S+))?
+ (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))?
+ (?:\s+master-repeat\s+(?P<master_repeat>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_garp,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "garp": {
+ "interval": "{{ interval if interval is defined else None }}",
+ "master_delay": "{{ master_delay if master_delay is defined else None }}",
+ "master_refresh": "{{ master_refresh if master_refresh is defined else None }}",
+ "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}",
+ "master_repeat": "{{ master_repeat if master_repeat is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.authentication",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \s+authentication
+ (?:\s+password\s+(?P<password>\S+))?
+ (?:\s+type\s+(?P<type>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_auth,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "authentication": {
+ "password": "{{ password if password is defined else None }}",
+ "type": "{{ type if type is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.transition_script",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \stransition-script
+ (?:\s+backup\s+(?P<backup>\S+))?
+ (?:\s+fault\s+(?P<fault>\S+))?
+ (?:\s+master\s+(?P<master>\S+))?
+ (?:\s+stop\s+(?P<stop>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_ts,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "transition_script": {
+ "backup": "{{ backup if backup is defined else None }}",
+ "fault": "{{ fault if fault is defined else None }}",
+ "master": "{{ master if master is defined else None }}",
+ "stop": "{{ stop if stop is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.health_check",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \shealth-check
+ (?:\s+failure-count\s+(?P<failure_count>\S+))?
+ (?:\s+interval\s+(?P<int>\S+))?
+ (?:\s+ping\s+(?P<ping>\S+))?
+ (?:\s+script\s+(?P<script>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_hc,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "health_check": {
+ "failure_count": "{{ failure_count if failure_count is defined else None }}",
+ "interval": "{{ int if int is defined else None }}",
+ "ping": "{{ ping if ping is defined else None }}",
+ "script": "{{ script if script is defined else None }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.track.interface",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \strack
+ (?:\s+interface\s+(?P<interface>\S+))?
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_track_list,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "track": {
+ "interface": "{{ [interface.strip(\"'\")] if interface is defined else [] }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.snmp",
+ "getval": re.compile(
+ r"""
+ ^set
+ \shigh-availability
+ \svrrp
+ \s(?P<snmp>snmp)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "high-availability vrrp snmp",
+ "result": {
+ "vrrp": {
+ "snmp": "{{ 'enabled' if snmp is defined else 'disabled' }}",
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \shigh-availability\svrrp\sgroup
+ \s(?P<gname>\S+)
+ \s(?P<disable>disable)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_bool,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "disable": "{{ True if disable is defined else False }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.no_preempt",
+ "getval": re.compile(
+ r"""
+ ^set
+ \shigh-availability\svrrp\sgroup
+ \s(?P<gname>\S+)
+ \s(?P<no_preempt>no-preempt)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_bool,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "no_preempt": "{{ True if no_preempt is defined else False }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.rfc3768_compatibility",
+ "getval": re.compile(
+ r"""
+ ^set
+ \shigh-availability\svrrp\sgroup
+ \s(?P<gname>\S+)
+ \s(?P<rfc3768_compatibility>rfc3768-compatibility)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_bool,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "rfc3768_compatibility": "{{ True if rfc3768_compatibility is defined else False }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "vrrp.groups.track.exclude_vrrp_interface",
+ "getval": re.compile(
+ r"""
+ ^set\shigh-availability\svrrp\sgroup
+ \s+(?P<gname>\S+)
+ \strack
+ \s(?P<exclude_vrrp_inter>exclude-vrrp-interface)
+ $
+ """,
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_vrrp_group_track_bool,
+ "result": {
+ "vrrp": {
+ "groups": {
+ "{{ gname }}": {
+ "name": "{{ gname }}",
+ "track": {
+ "exclude_vrrp_interface": "{{ True if exclude_vrrp_inter is defined else False }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/hostname.py b/plugins/module_utils/network/vyos/rm_templates/hostname.py
index 29ab00f6..b7e56310 100644
--- a/plugins/module_utils/network/vyos/rm_templates/hostname.py
+++ b/plugins/module_utils/network/vyos/rm_templates/hostname.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global.py b/plugins/module_utils/network/vyos/rm_templates/logging_global.py
index 516e270b..07ad1f68 100644
--- a/plugins/module_utils/network/vyos/rm_templates/logging_global.py
+++ b/plugins/module_utils/network/vyos/rm_templates/logging_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py
new file mode 100644
index 00000000..3216747d
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py
@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+# Copyright 2021 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+def tmplt_params(config_data):
+ def templt_common(val, tmplt):
+ if val.get("facility"):
+ tmplt += " facility {facility}".format(facility=val["facility"])
+ if val.get("severity"):
+ tmplt += " level {level}".format(level=val["severity"])
+ return tmplt
+
+ tmplt = ""
+ if config_data.get("global_params"):
+ val = config_data.get("global_params")
+ tmplt += "system syslog local"
+ tmplt = templt_common(val.get("facilities", {}), tmplt)
+ elif config_data.get("console"):
+ val = config_data.get("console")
+ tmplt += "system syslog console"
+ tmplt = templt_common(val.get("facilities", {}), tmplt)
+ elif config_data.get("hosts"):
+ val = config_data.get("hosts")
+ if val.get("hostname") and not val.get("port") and not val.get("protocol"):
+ tmplt += "system syslog remote {hostname}".format(hostname=val["hostname"])
+ if val.get("facilities"):
+ tmplt = templt_common(val.get("facilities"), tmplt)
+ return tmplt
+
+
+class Logging_globalTemplate15(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Logging_globalTemplate15, self).__init__(
+ lines=lines,
+ tmplt=self,
+ prefix=prefix,
+ module=module,
+ )
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "syslog.state",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem
+ (\s(?P<syslog>syslog))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog",
+ "result": {
+ "syslog": {
+ "state": "{{ 'enabled' if syslog is defined else 'disabled' }}",
+ },
+ },
+ },
+ {
+ "name": "console.state",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog
+ (\s(?P<console>console))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog console",
+ "result": {
+ "console": {
+ "state": "{{ 'enabled' if console is defined else 'disabled' }}",
+ },
+ },
+ },
+ {
+ "name": "console.facilities",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\sconsole\sfacility
+ (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))?
+ (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))?
+ $""", re.VERBOSE,
+ ),
+ "setval": tmplt_params,
+ "remval": "system syslog console facility {{ console.facilities.facility }}",
+ "result": {
+ "console": {
+ "facilities": [
+ {
+ "facility": "{{ facility }}",
+ "severity": "{{ level }}",
+ },
+ ],
+ },
+ },
+ },
+ {
+ "name": "global_params.state",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog
+ (\s(?P<local>local))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog local",
+ "result": {
+ "global_params": {
+ "state": "{{ 'enabled' if local is defined else 'disabled' }}",
+ },
+ },
+ },
+ {
+ "name": "global_params.marker_interval",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\smarker\sinterval
+ (\s(?P<marker_interval>'(\d+)'))?
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog marker interval {{ global_params.marker_interval }}",
+ "remval": "system syslog marker",
+ "result": {
+ "global_params": {
+ "marker_interval": "{{ marker_interval }}",
+ },
+ },
+ },
+ {
+ "name": "global_params.preserve_fqdn",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog
+ (\s(?P<preserve_fqdn>preserve-fqdn))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog preserve-fqdn",
+ "result": {
+ "global_params": {
+ "preserve_fqdn": "{{ True if preserve_fqdn is defined }}",
+ },
+ },
+ },
+ {
+ "name": "global_params.facilities",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\slocal\sfacility
+ (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))?
+ (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))?
+ $""", re.VERBOSE,
+ ),
+ "setval": tmplt_params,
+ "remval": "system syslog local facility {{ global_params.facilities.facility }}",
+ "result": {
+ "global_params": {
+ "facilities": [
+ {
+ "facility": "{{ facility }}",
+ "severity": "{{ level }}",
+ },
+ ],
+ },
+ },
+ },
+ {
+ "name": "hosts.port",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\sremote
+ (\s(?P<hostname>\S+))
+ (\sport\s(?P<port>'(\d+)'))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog remote {{ hosts.hostname }} port {{ hosts.port }}",
+ "result": {
+ "hosts": {
+ "{{ hostname }}": {
+ "hostname": "{{ hostname }}",
+ "port": "{{ port }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "hosts.protocol",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\sremote
+ (\s(?P<hostname>\S+))
+ (\sprotocol\s(?P<protocol>'(udp|tcp)'))
+ $""", re.VERBOSE,
+ ),
+ "setval": "system syslog remote {{ hosts.hostname }} protocol {{ hosts.protocol }}",
+ "result": {
+ "hosts": {
+ "{{ hostname }}": {
+ "hostname": "{{ hostname }}",
+ "protocol": "{{ protocol }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "hosts",
+ "getval": re.compile(
+ r"""
+ ^set\ssystem\ssyslog\sremote
+ (\s(?P<hostname>\S+))
+ (\sfacility\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))
+ (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))?
+ $""", re.VERBOSE,
+ ),
+ "setval": tmplt_params,
+ "remval": "system syslog remote {{ hosts.hostname }}",
+ "result": {
+ "hosts": {
+ "{{ hostname }}": {
+ "hostname": "{{ hostname }}",
+ "facilities": [
+ {
+ "facility": "{{ facility }}",
+ "severity": "{{ level }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py
new file mode 100644
index 00000000..94af8696
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/nat.py
@@ -0,0 +1,1186 @@
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+def _tmplt_nat_rule_addr_sub(config_data):
+ """Generate address/fqdn/prefix/port/group commands for destination or source sub-dict."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ atype = config_data["atype"]
+ sub = config_data["sub"]
+
+ base = f"{nat} {type_} rule {rid} {atype}"
+ commands = []
+
+ for field in ("address", "fqdn", "prefix", "port"):
+ if sub.get(field) is not None:
+ commands.append(f"{base} {field} {sub[field]}")
+
+ for gtype in ("address_group", "domain_group", "mac_group", "network_group", "port_group"):
+ if sub.get(gtype) is not None:
+ commands.append(f"{base} group {gtype.replace('_', '-')} {sub[gtype]}")
+
+ return commands
+
+
+def _tmplt_nat_rule_translation(config_data):
+ """Generate translation commands."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ trans = config_data["translation"]
+
+ base = f"{nat} {type_} rule {rid} translation"
+ commands = []
+
+ if trans.get("address") is not None:
+ commands.append(f"{base} address {trans['address']}")
+
+ if trans.get("port") is not None:
+ commands.append(f"{base} port {trans['port']}")
+
+ if trans.get("redirect_port") is not None:
+ commands.append(f"{base} redirect port {trans['redirect_port']}")
+
+ if trans.get("address_mapping") is not None:
+ commands.append(f"{base} options address-mapping {trans['address_mapping']}")
+
+ if trans.get("port_mapping") is not None:
+ commands.append(f"{base} options port-mapping {trans['port_mapping']}")
+
+ return commands
+
+
+def _tmplt_nat64_translation_pool(config_data):
+ """Generate all nat64 translation pool commands from a single call."""
+ nat = config_data["nat"]
+ type_ = config_data["type"]
+ rid = config_data["id"]
+ pool_id = config_data["pool_id"]
+ pool = config_data["pool"]
+
+ base = f"{nat} {type_} rule {rid} translation pool {pool_id}"
+ commands = []
+
+ if pool.get("address") is not None:
+ commands.append(f"{base} address {pool['address']}")
+ if pool.get("description") is not None:
+ commands.append(f"{base} description '{pool['description']}'")
+ if pool.get("port") is not None:
+ commands.append(f"{base} port {pool['port']}")
+ if pool.get("protocol") is not None:
+ commands.append(f"{base} protocol {pool['protocol']}")
+ if pool.get("disable"):
+ commands.append(f"{base} disable")
+
+ return commands
+
+
+class NatTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(NatTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module)
+
+ # fmt: off
+ PARSERS = [
+
+ # -------------------------
+ # CGNAT
+ # -------------------------
+ {
+ "name": "cgnat_log_allocation",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+log-allocation
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat log-allocation",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "log_allocation": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+range
+ \s+(?P<range>\S+)(?:\s+seq\s+(?P<seq>\d+))?
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined and seq %} seq {{ seq }}{% endif %}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "range": [
+ {
+ "value": "{{ range }}",
+ "seq": "{{ seq }}",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_port_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+external-port-range
+ \s+(?P<range>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} external-port-range {{ range }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "external_port_range": "{{ range }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_external_per_user",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+external
+ \s+(?P<name>\S+)
+ \s+per-user-limit
+ \s+port
+ \s+(?P<limit>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool external {{ name }} per-user-limit port {{ limit }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "external": [
+ {
+ "name": "{{ name }}",
+ "per_user_limit": {"port": "{{ limit }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_pool_internal_range",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+pool
+ \s+internal
+ \s+(?P<name>\S+)
+ \s+range
+ \s+(?P<range>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat pool internal {{ name }} range {{ range }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "pool": {
+ "internal": [
+ {
+ "name": "{{ name }}",
+ "range": ["{{ range }}"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_rule_source_pool",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+rule
+ \s+(?P<id>\d+)
+ \s+source
+ \s+pool
+ \s+(?P<pool>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat rule {{ id }} source pool {{ pool }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "source": {"pool": "{{ pool }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "cgnat_rule_translation_pool",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+cgnat
+ \s+rule
+ \s+(?P<id>\d+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}",
+ "result": {
+ "nat": {
+ "cgnat": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"pool": "{{ pool }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+
+ # -------------------------
+ # GENERIC NAT
+ # -------------------------
+ {
+ "name": "nat_type_description",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+description
+ \s+(?P<description>.+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} description '{{ description }}'",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "description": "{{ description }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_protocol",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+protocol
+ \s+(?P<protocol>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "protocol": "{{ protocol }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+disable
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} disable",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "disable": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_exclude",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+exclude
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} exclude",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "exclude": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_log",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+log
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} log",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "log": True,
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"address": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_prefix",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+prefix
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"prefix": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_fqdn",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+fqdn
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"fqdn": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {"port": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_address_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+(?P<atype>destination|source)
+ \s+group
+ \s+(?P<gtype>address-group|domain-group|mac-group|network-group|port-group)
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_addr_sub,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "{{ atype }}": {
+ "{{ gtype | replace('-', '_') }}": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"address": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {"port": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_options",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+options
+ \s+(?P<opt>address-mapping|port-mapping)
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "{{ opt | replace('-', '_') }}": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_translation_redirect",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+redirect
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": _tmplt_nat_rule_translation,
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "redirect_port": "{{ value }}",
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_inbound_interface_name",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat {{ type }} rule {{ id }} inbound-interface name {{ value }}",
+ "result": {
+ "nat": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_inbound_interface_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+group
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat {{ type }} rule {{ id }} inbound-interface group {{ value }}",
+ "result": {
+ "nat": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"group": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_static_inbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat
+ \s+static
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat static rule {{ id }} inbound-interface {{ value }}",
+ "result": {
+ "nat": {
+ "static": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": "{{ value }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat6x_inbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+inbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "inbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_outbound_interface",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+outbound-interface
+ \s+name
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface name {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "outbound_interface": {"name": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_outbound_interface_group",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source|static)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+outbound-interface
+ \s+group
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface group {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "outbound_interface": {"group": "{{ value }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_packet_type",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+packet-type
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "packet_type": "{{ value }}",
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_lb_backend",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+load-balance
+ \s+backend
+ \s+(?P<ip>\S+)
+ \s+weight
+ \s+(?P<weight>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance backend {{ ip }} weight {{ weight }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "load_balance": {
+ "backend": [
+ {"ip": "{{ ip }}", "weight": "{{ weight }}"},
+ ],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat_type_lb_hash",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+(?P<nat>nat|nat64|nat66)
+ \s+(?P<type>destination|source)
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+load-balance
+ \s+hash
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}",
+ "result": {
+ "{{ nat }}": {
+ "{{ type }}": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "load_balance": {"hash": ["{{ value }}"]},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_match_mark",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+match
+ \s+mark
+ \s+(?P<mark>\d+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} match mark {{ mark }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "match": {"mark": "{{ mark }}"},
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool",
+ "getval": re.compile(r"^$"), # never matches — setval only
+ "setval": _tmplt_nat64_translation_pool,
+ "result": {},
+ },
+ {
+ "name": "nat64_translation_pool_address",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+address
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} address {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "address": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+
+ {
+ "name": "nat64_translation_pool_description",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+description
+ \s+(?P<value>.+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} description '{{ value }}'",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "description": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_disable",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+disable
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} disable",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "disable": True}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_port",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+port
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} port {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "port": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ "name": "nat64_translation_pool_protocol",
+ "getval": re.compile(
+ r"""
+ ^set
+ \s+nat64
+ \s+source
+ \s+rule
+ \s+(?P<id>\S+)
+ \s+translation
+ \s+pool
+ \s+(?P<pool_id>\d+)
+ \s+protocol
+ \s+(?P<value>\S+)
+ $""",
+ re.VERBOSE,
+ ),
+ "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} protocol {{ value }}",
+ "result": {
+ "nat64": {
+ "source": {
+ "rule": [
+ {
+ "id": "{{ id }}",
+ "translation": {
+ "pool": [{"id": "{{ pool_id }}", "protocol": "{{ value }}"}],
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py
index e9d8a0cb..b83a3e1c 100644
--- a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py
+++ b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py
index af04da3b..5183aec4 100644
--- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py
+++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py
index 484d7c44..8d09011f 100644
--- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py
+++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py
@@ -32,9 +32,7 @@ def _get_parameters(data):
def _tmplt_ospf_int_delete(config_data):
params = _get_parameters(config_data["address_family"])
- command = (
- "protocols " + params[0] + " interface {name}".format(**config_data)
- )
+ command = "protocols " + params[0] + " interface {name}".format(**config_data)
return command
@@ -82,10 +80,7 @@ def _tmplt_ospf_int_auth_md5(config_data):
def _tmplt_ospf_int_auth_md5_delete(config_data):
params = _get_parameters(config_data["address_family"])
command = (
- "protocols "
- + params[0]
- + " interface {name}".format(**config_data)
- + " authentication"
+ "protocols " + params[0] + " interface {name}".format(**config_data) + " authentication"
)
return command
@@ -129,12 +124,7 @@ def _tmplt_ospf_int_dead_interval(config_data):
def _tmplt_ospf_int_mtu_ignore(config_data):
params = _get_parameters(config_data["address_family"])
- command = (
- "protocols "
- + params[0]
- + " interface {name}".format(**config_data)
- + " mtu-ignore"
- )
+ command = "protocols " + params[0] + " interface {name}".format(**config_data) + " mtu-ignore"
return command
@@ -213,12 +203,7 @@ def _tmplt_ospf_int_instance(config_data):
def _tmplt_ospf_int_passive(config_data):
params = _get_parameters(config_data["address_family"])
- command = (
- "protocols "
- + params[0]
- + " interface {name}".format(**config_data)
- + " passive"
- )
+ command = "protocols " + params[0] + " interface {name}".format(**config_data) + " passive"
return command
diff --git a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py
index 0e99cfea..0e071199 100644
--- a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py
+++ b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps.py b/plugins/module_utils/network/vyos/rm_templates/route_maps.py
index 51d0ea1c..fd8fdd9d 100644
--- a/plugins/module_utils/network/vyos/rm_templates/route_maps.py
+++ b/plugins/module_utils/network/vyos/rm_templates/route_maps.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py
index ea61a555..cf2d6b67 100644
--- a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py
+++ b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py
index ff1c6465..bd76a5ae 100644
--- a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py
+++ b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py
new file mode 100644
index 00000000..ff889526
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py
@@ -0,0 +1,997 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The VPN IPSEC parser templates file. This contains
+a list of parser definitions and associated functions that
+facilitates both facts gathering and native command generation for
+the given network resource.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+class Vpn_ipsecTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Vpn_ipsecTemplate, self).__init__(
+ lines=lines,
+ tmplt=self,
+ prefix=prefix,
+ module=module,
+ )
+
+ # fmt: off
+ PARSERS = [
+ # ---------------------------------------------------------------
+ # esp-group
+ # ---------------------------------------------------------------
+ {
+ "name": "esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal.encryption",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sencryption\s'?(?P<encryption>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "encryption": "{{ encryption }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal.hash",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \shash\s'?(?P<hash>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "hash": "{{ hash }}",
+ },
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # ike-group
+ # ---------------------------------------------------------------
+ {
+ "name": "ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.key_exchange",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \skey-exchange\s'?(?P<key_exchange>\w+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} key-exchange {{ key_exchange }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "key_exchange": "{{ key_exchange }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.dh_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sdh-group\s'?(?P<dh_group>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} dh-group {{ dh_group }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "dh_group": "{{ dh_group }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.encryption",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sencryption\s'?(?P<encryption>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "encryption": "{{ encryption }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.hash",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \shash\s'?(?P<hash>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "hash": "{{ hash }}",
+ },
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication psk
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.psk",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \sid\s'?(?P<id>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} id {{ id }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "id": ["{{ id }}"],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \ssecret\s'?(?P<secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} secret '{{ secret }}'",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "secret": "{{ secret }}",
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # profile
+ # ---------------------------------------------------------------
+ {
+ "name": "profile",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.authentication.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sauthentication\smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} authentication mode {{ mode }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "authentication": {
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.authentication.pre_shared_secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sauthentication\spre-shared-secret\s'?(?P<pre_shared_secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ pre_shared_secret }}'",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "authentication": {
+ "pre_shared_secret": "{{ pre_shared_secret }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.bind_tunnel",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sbind\stunnel\s'?(?P<bind_tunnel>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} bind tunnel {{ bind_tunnel }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "bind_tunnel": ["{{ bind_tunnel }}"],
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sesp-group\s'?(?P<esp_group>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} esp-group {{ esp_group }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "esp_group": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sike-group\s'?(?P<ike_group>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} ike-group {{ ike_group }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "ike_group": "{{ ike_group }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # ike-group: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "ike_group.close_action",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sclose-action\s'?(?P<close_action>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} close-action {{ close_action }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "close_action": "{{ close_action }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.action",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\saction\s'?(?P<action>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection action {{ action }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"action": "{{ action }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.interval",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\sinterval\s'?(?P<interval>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection interval {{ interval }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"interval": "{{ interval }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.timeout",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\stimeout\s'?(?P<timeout>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection timeout {{ timeout }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"timeout": "{{ timeout }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.disable_mobike",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdisable-mobike
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} disable-mobike",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "disable_mobike": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.ikev2_reauth",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sikev2-reauth
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} ikev2-reauth",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "ikev2_reauth": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.lifetime",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \slifetime\s'?(?P<lifetime>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} lifetime {{ lifetime }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "lifetime": "{{ lifetime }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} mode {{ mode }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # esp-group: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "esp_group.compression",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \scompression
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} compression",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "compression": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.disable_rekey",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sdisable-rekey
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} disable-rekey",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "disable_rekey": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.life_bytes",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slife-bytes\s'?(?P<life_bytes>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} life-bytes {{ life_bytes }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "life_bytes": "{{ life_bytes }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.life_packets",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slife-packets\s'?(?P<life_packets>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} life-packets {{ life_packets }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "life_packets": "{{ life_packets }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.lifetime",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slifetime\s'?(?P<lifetime>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} lifetime {{ lifetime }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "lifetime": "{{ lifetime }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} mode {{ mode }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.pfs",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \spfs\s'?(?P<pfs>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} pfs {{ pfs }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "pfs": "{{ pfs }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication.psk: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.psk.secret_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \ssecret-type\s'?(?P<secret_type>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} secret-type {{ secret_type }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "secret_type": "{{ secret_type }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.dhcp_interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \sdhcp-interface\s'?(?P<dhcp_interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} dhcp-interface {{ dhcp_interface }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "dhcp_interface": ["{{ dhcp_interface }}"],
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication.ppk
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.ppk",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \sid\s'?(?P<id>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} id {{ id }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "id": ["{{ id }}"],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \ssecret\s'?(?P<secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} secret '{{ secret }}'",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "secret": "{{ secret }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.secret_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \ssecret-type\s'?(?P<secret_type>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} secret-type {{ secret_type }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "secret_type": "{{ secret_type }}",
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # profile: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "profile.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sdisable
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} disable",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "disable": True,
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # top-level: interface, log, options, disable_uniqreqids
+ # ---------------------------------------------------------------
+ {
+ "name": "interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sinterface\s'?(?P<interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec interface {{ interface }}",
+ "result": {
+ "interface": ["{{ interface }}"],
+ },
+ },
+ {
+ "name": "log.level",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\slog\slevel\s'?(?P<level>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec log level {{ level }}",
+ "result": {
+ "log": {"level": "{{ level }}"},
+ },
+ },
+ {
+ "name": "log.subsystem",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\slog\ssubsystem\s'?(?P<subsystem>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec log subsystem {{ subsystem }}",
+ "result": {
+ "log": {"subsystem": ["{{ subsystem }}"]},
+ },
+ },
+ {
+ "name": "options.disable_route_autoinstall",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sdisable-route-autoinstall
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options disable-route-autoinstall",
+ "result": {
+ "options": {"disable_route_autoinstall": True},
+ },
+ },
+ {
+ "name": "options.flexvpn",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sflexvpn
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options flexvpn",
+ "result": {
+ "options": {"flexvpn": True},
+ },
+ },
+ {
+ "name": "options.interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sinterface\s'?(?P<interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options interface {{ interface }}",
+ "result": {
+ "options": {"interface": "{{ interface }}"},
+ },
+ },
+ {
+ "name": "options.retransmission.attempts",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\sattempts\s'?(?P<attempts>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission attempts {{ attempts }}",
+ "result": {
+ "options": {"retransmission": {"attempts": "{{ attempts }}"}},
+ },
+ },
+ {
+ "name": "options.retransmission.base",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\sbase\s'?(?P<base>[\d.]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission base {{ base }}",
+ "result": {
+ "options": {"retransmission": {"base": "{{ base }}"}},
+ },
+ },
+ {
+ "name": "options.retransmission.timeout",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\stimeout\s'?(?P<timeout>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission timeout {{ timeout }}",
+ "result": {
+ "options": {"retransmission": {"timeout": "{{ timeout }}"}},
+ },
+ },
+ {
+ "name": "options.virtual_ip",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\svirtual-ip
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options virtual-ip",
+ "result": {
+ "options": {"virtual_ip": True},
+ },
+ },
+ {
+ "name": "disable_uniqreqids",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sdisable-uniqreqids
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec disable-uniqreqids",
+ "result": {
+ "disable_uniqreqids": True,
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..4319019c
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py
@@ -0,0 +1,670 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+class Vpn_ipsec_s2sTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Vpn_ipsec_s2sTemplate, self).__init__(
+ lines=lines,
+ tmplt=self,
+ module=module,
+ prefix=prefix,
+ )
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "peer",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }}",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)\sdisable$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} disable",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "disable": True}}},
+ },
+ },
+ {
+ "name": "peer.authentication.local_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\slocal-id\s'(?P<local_id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication local-id '{{ local_id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"local_id": "{{ local_id }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.remote_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sremote-id\s'(?P<remote_id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication remote-id '{{ remote_id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"remote_id": "{{ remote_id }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\smode\s'(?P<mode>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication mode '{{ mode }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"mode": "{{ mode }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.use_x509_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\suse-x509-id$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication use-x509-id",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"use_x509_id": True}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.ppk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sppk\sid\s'(?P<id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk id '{{ id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"id": "{{ id }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.ppk.required",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sppk\srequired$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk required",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"required": True}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.local_key",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\slocal-key\s'(?P<local_key>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa local-key '{{ local_key }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"local_key": "{{ local_key }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.remote_key",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\sremote-key\s'(?P<remote_key>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa remote-key '{{ remote_key }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"remote_key": "{{ remote_key }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.passphrase",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\spassphrase\s'(?P<passphrase>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa passphrase '{{ passphrase }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"passphrase": "{{ passphrase }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.certificate",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\scertificate\s'(?P<certificate>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 certificate '{{ certificate }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"certificate": "{{ certificate }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.passphrase",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\spassphrase\s'(?P<passphrase>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 passphrase '{{ passphrase }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"passphrase": "{{ passphrase }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.ca_certificate",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\sca-certificate\s'(?P<ca_certificate>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 ca-certificate '{{ ca_certificate }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"ca_certificate": ["{{ ca_certificate }}"]}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.childless",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \schildless\s'(?P<childless>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} childless '{{ childless }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "childless": "{{ childless }}"}}},
+ },
+ },
+ {
+ "name": "peer.connection_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sconnection-type\s'(?P<connection_type>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} connection-type '{{ connection_type }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "connection_type": "{{ connection_type }}"}}},
+ },
+ },
+ {
+ "name": "peer.default_esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdefault-esp-group\s'(?P<default_esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} default-esp-group '{{ default_esp_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "default_esp_group": "{{ default_esp_group }}"}}},
+ },
+ },
+ {
+ "name": "peer.description",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdescription\s'(?P<description>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} description '{{ description }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "description": "{{ description }}"}}},
+ },
+ },
+ {
+ "name": "peer.dhcp_interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdhcp-interface\s'(?P<dhcp_interface>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} dhcp-interface '{{ dhcp_interface }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "dhcp_interface": "{{ dhcp_interface }}"}}},
+ },
+ },
+ {
+ "name": "peer.force_udp_encapsulation",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sforce-udp-encapsulation$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} force-udp-encapsulation",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "force_udp_encapsulation": True}}},
+ },
+ },
+ {
+ "name": "peer.ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sike-group\s'(?P<ike_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} ike-group '{{ ike_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ike_group": "{{ ike_group }}"}}},
+ },
+ },
+ {
+ "name": "peer.ikev2_reauth",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sikev2-reauth\s'(?P<ikev2_reauth>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} ikev2-reauth '{{ ikev2_reauth }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ikev2_reauth": "{{ ikev2_reauth }}"}}},
+ },
+ },
+ {
+ "name": "peer.local_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \slocal-address\s'(?P<local_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} local-address '{{ local_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "local_address": "{{ local_address }}"}}},
+ },
+ },
+ {
+ "name": "peer.remote_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sremote-address\s'(?P<remote_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} remote-address '{{ remote_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "remote_address": ["{{ remote_address }}"]}}},
+ },
+ },
+ {
+ "name": "peer.replay_window",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sreplay-window\s'(?P<replay_window>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} replay-window '{{ replay_window }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "replay_window": "{{ replay_window }}"}}},
+ },
+ },
+ {
+ "name": "peer.virtual_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svirtual-address\s'(?P<virtual_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} virtual-address '{{ virtual_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "virtual_address": ["{{ virtual_address }}"]}}},
+ },
+ },
+ {
+ "name": "peer.tunnel",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }}",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sdisable$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} disable",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "disable": True,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sesp-group\s'(?P<esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} esp-group '{{ esp_group }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "esp_group": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.protocol",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sprotocol\s'(?P<protocol>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} protocol '{{ protocol }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "protocol": "{{ protocol }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.priority",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\spriority\s'(?P<priority>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} priority '{{ priority }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "priority": "{{ priority }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.local.port",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\slocal\sport\s'(?P<port>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local port '{{ port }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "local": {"port": "{{ port }}"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.local.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\slocal\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.remote.port",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sremote\sport\s'(?P<port>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote port '{{ port }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "remote": {"port": "{{ port }}"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.remote.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sremote\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.vti.bind",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\sbind\s'(?P<bind>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti bind '{{ bind }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"bind": "{{ bind }}"}}}},
+ },
+ },
+ {
+ "name": "peer.vti.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\sesp-group\s'(?P<esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti esp-group '{{ esp_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"esp_group": "{{ esp_group }}"}}}},
+ },
+ },
+ {
+ "name": "peer.vti.traffic_selector.local.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\straffic-selector\slocal\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector local prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "vti": {"traffic_selector": {"local": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.vti.traffic_selector.remote.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\straffic-selector\sremote\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector remote prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "vti": {"traffic_selector": {"remote": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/vrf.py b/plugins/module_utils/network/vyos/rm_templates/vrf.py
index 5171404e..79928547 100644
--- a/plugins/module_utils/network/vyos/rm_templates/vrf.py
+++ b/plugins/module_utils/network/vyos/rm_templates/vrf.py
@@ -5,7 +5,6 @@
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
"""
diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py
index 78445c02..89f12773 100644
--- a/plugins/module_utils/network/vyos/utils/utils.py
+++ b/plugins/module_utils/network/vyos/utils/utils.py
@@ -6,11 +6,9 @@
# utils
from __future__ import absolute_import, division, print_function
-
__metaclass__ = type
from ansible.module_utils.basic import missing_required_lib
-
try:
import ipaddress
@@ -288,21 +286,60 @@ def in_target_not_none(h, key):
def combine(a, b, recursive=False, list_merge="replace"):
+ """Merge dict ``b`` into dict ``a``, returning a new dict.
+
+ :param a: Base dictionary.
+ :param b: Dictionary whose values take precedence over ``a``.
+ :param recursive: When True, nested dicts are merged recursively rather
+ than replaced wholesale.
+ :param list_merge: Controls how list values are combined when the same key
+ exists in both dicts. Supported modes:
+
+ - ``"replace"`` *(default)* — ``b``'s list replaces ``a``'s list.
+ - ``"append"`` — ``b``'s list is appended to ``a``'s list (duplicates
+ kept).
+ - ``"prepend"`` — ``b``'s list is prepended to ``a``'s list
+ (duplicates kept).
+ - ``"append_rp"`` — like ``"append"`` but duplicates are removed,
+ preserving the first occurrence (rp = remove-preserve).
+ - ``"prepend_rp"`` — like ``"prepend"`` but duplicates are removed,
+ preserving the first occurrence.
+
+ Passing any other value raises ``ValueError``.
+ :returns: New merged dict.
+ :raises ValueError: If either argument is not a dict, or if an
+ unsupported ``list_merge`` mode is given.
"""
- Merge two dictionaries (shallow or deep).
- :param a: dict
- :param b: dict
- :param recursive: bool, deep merge
- :param list_merge: str, only 'replace' is supported (default Ansible behavior)
- """
+
if not isinstance(a, dict) or not isinstance(b, dict):
raise ValueError("combine expects two dictionaries")
result = a.copy()
for k, v in b.items():
- if recursive and k in result and isinstance(result[k], dict) and isinstance(v, dict):
- result[k] = combine(result[k], v, recursive=True, list_merge=list_merge)
+ if k in result:
+ # dict merge
+ if recursive and isinstance(result[k], dict) and isinstance(v, dict):
+ result[k] = combine(result[k], v, recursive=True, list_merge=list_merge)
+
+ # list merge
+ elif isinstance(result[k], list) and isinstance(v, list):
+ if list_merge == "replace":
+ result[k] = v
+ elif list_merge == "append":
+ result[k] = result[k] + v
+ elif list_merge == "prepend":
+ result[k] = v + result[k]
+ elif list_merge == "append_rp":
+ result[k] = list(dict.fromkeys(result[k] + v))
+ elif list_merge == "prepend_rp":
+ result[k] = list(dict.fromkeys(v + result[k]))
+ else:
+ raise ValueError(f"Unsupported list_merge mode: {list_merge}")
+
+ # everything else
+ else:
+ result[k] = v
else:
result[k] = v
diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py
index 4987d6b0..4983221e 100644
--- a/plugins/module_utils/network/vyos/vyos.py
+++ b/plugins/module_utils/network/vyos/vyos.py
@@ -87,6 +87,30 @@ def get_config(module, flags=None, format=None):
return cfg
+def copy_file(module, source, destination, proto="scp"):
+ """Copy a local file to the remote device over the existing network_cli
+ SSH session, using netcommon's generic connection-level file transfer
+ RPC (the same mechanism ansible.netcommon.net_put uses).
+
+ Requires the device to have SCP/SFTP reachable over the same SSH
+ session used for network_cli. Mirrors the calling convention of
+ cisco.iosxr's module_utils copy_file(module, source, destination, proto),
+ confirmed against cisco.iosxr's iosxr_config.py call site:
+ copy_file(module, src, dst, "sftp").
+ """
+ connection = get_connection(module)
+ try:
+ timeout = connection.get_option("persistent_command_timeout")
+ connection.copy_file(
+ source=source,
+ destination=destination,
+ proto=proto,
+ timeout=timeout,
+ )
+ except ConnectionError as exc:
+ module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))
+
+
def run_commands(module, commands, check_rc=True):
connection = get_connection(module)
try:
@@ -101,7 +125,10 @@ def load_config(module, commands, commit=False, comment=None, confirm=None):
try:
response = connection.edit_config(
- candidate=commands, commit=commit, comment=comment, confirm=confirm
+ candidate=commands,
+ commit=commit,
+ comment=comment,
+ confirm=confirm,
)
except ConnectionError as exc:
module.fail_json(msg=to_text(exc, errors="surrogate_then_replace"))
diff --git a/plugins/module_utils/network/vyos/vyos_file.py b/plugins/module_utils/network/vyos/vyos_file.py
new file mode 100644
index 00000000..2b946c20
--- /dev/null
+++ b/plugins/module_utils/network/vyos/vyos_file.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2026, VyOS maintainers and contributors
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import hashlib
+import re
+
+
+STAT_RE = re.compile(r"^(?P<mode>\d+)\s+(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)$")
+
+
+def parse_stat(output):
+ """Parse `stat --format='%a %U %G %s' <path>` output.
+ Returns None if the path doesn't exist (caller checks rc/stderr first).
+ """
+ m = STAT_RE.match(output.strip())
+ if not m:
+ return None
+ d = m.groupdict()
+ return {
+ "mode": d["mode"].zfill(4)[-4:],
+ "owner": d["owner"],
+ "group": d["group"],
+ "size": int(d["size"]),
+ }
+
+
+def _normalize_mode(mode):
+ if mode is None:
+ return None
+ return str(mode).zfill(4)[-4:]
+
+
+def build_want(params, local_content_hash=None):
+ return {
+ "dest": params["dest"],
+ "state": params.get("state", "present"),
+ "owner": params.get("owner"),
+ "group": params.get("group"),
+ "mode": _normalize_mode(params.get("mode")),
+ "content_hash": local_content_hash,
+ }
+
+
+def diff_want_have(want, have):
+ """Returns dict of {field: (have_val, want_val)} for fields that differ.
+ Identity is `dest`, not a config-tree path — this compares a stat-shaped
+ dict, not config lines.
+ """
+ diff = {}
+ if want["state"] == "absent":
+ if have is not None:
+ diff["state"] = (have, "absent")
+ return diff
+
+ if have is None:
+ diff["state"] = (None, "present")
+ for f in ("owner", "group", "mode"):
+ if want.get(f) is not None:
+ diff[f] = (None, want[f])
+ if want.get("content_hash"):
+ diff["content"] = (None, want["content_hash"])
+ return diff
+
+ for f in ("owner", "group"):
+ if want.get(f) is not None and want[f] != have.get(f):
+ diff[f] = (have.get(f), want[f])
+
+ if want.get("mode") is not None:
+ want_mode = want["mode"]
+ have_mode = have.get("mode")
+ if want_mode[0] == "0":
+ # Caller didn't request specific setuid/setgid/sticky bits —
+ # don't fight VyOS's own conventions (e.g. /config/auth is
+ # deliberately setgid vyattacfg; see vyos.dev T2713). Compare
+ # only the rwx digits unless the caller explicitly asked for a
+ # non-zero leading digit.
+ if want_mode[-3:] != have_mode[-3:]:
+ diff["mode"] = (have_mode, want_mode)
+ elif want_mode != have_mode:
+ diff["mode"] = (have_mode, want_mode)
+
+ if want.get("content_hash") and want["content_hash"] != have.get("content_hash"):
+ diff["content"] = (have.get("content_hash"), want["content_hash"])
+
+ return diff
+
+
+def local_sha256(path):
+ h = hashlib.sha256()
+ with open(path, "rb") as f:
+ for chunk in iter(lambda: f.read(65536), b""):
+ h.update(chunk)
+ return h.hexdigest()