diff options
Diffstat (limited to 'plugins/module_utils/network')
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() |
