diff options
Diffstat (limited to 'plugins/module_utils/network')
116 files changed, 12384 insertions, 897 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 6ae17585..0378a65b 100644 --- a/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/argspec/firewall_rules/firewall_rules.py @@ -25,10 +25,8 @@ The arg spec for the vyos_firewall_rules module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -56,6 +54,8 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "reject", "accept", "jump", + "return", + "continue", ], "type": "str", }, @@ -83,6 +83,7 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "continue", "return", "jump", + "offload", "queue", "synproxy", ], @@ -210,6 +211,7 @@ class Firewall_rulesArgs(object): # pylint: disable=R0903 "required": True, "type": "int", }, + "offload_target": {"type": "str"}, "outbound_interface": { "options": { "group": {"type": "str"}, diff --git a/plugins/module_utils/network/vyos/argspec/ha/__init__.py b/plugins/module_utils/network/vyos/argspec/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/ha/ha.py b/plugins/module_utils/network/vyos/argspec/ha/ha.py new file mode 100644 index 00000000..4fe431cd --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/ha/ha.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_ha module +""" + + +class HaArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_ha module""" + + argument_spec = { + "config": { + "type": "dict", + "required": False, + "options": { + "disable": {"type": "bool", "default": False}, + "virtual_servers": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "str"}, + "algorithm": {"type": "str"}, + "delay_loop": {"type": "int"}, + "forward_method": {"type": "str", "choices": ["direct", "nat"]}, + "fwmark": {"type": "int"}, + "persistence_timeout": {"type": "int"}, + "port": {"type": "int"}, + "protocol": {"type": "str", "choices": ["tcp", "udp"]}, + "real_server": { + "type": "list", + "elements": "dict", + "options": { + "address": {"type": "str", "required": True}, + "port": {"type": "int"}, + "connection_timeout": {"type": "int"}, + "health_check_script": {"type": "str"}, + }, + }, + }, + }, + "vrrp": { + "type": "dict", + "options": { + "global_parameters": { + "type": "dict", + "options": { + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "startup_delay": {"type": "int"}, + "version": {"type": "str"}, + }, + }, + "groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "address": {"type": "list", "elements": "str"}, + "advertise_interval": {"type": "int"}, + "authentication": { + "type": "dict", + "options": { + "password": {"type": "str", "no_log": True}, + "type": {"type": "str"}, + }, + }, + "description": {"type": "str"}, + "disable": {"type": "bool", "default": False}, + "excluded_address": {"type": "list", "elements": "str"}, + "garp": { + "type": "dict", + "options": { + "interval": {"type": "int"}, + "master_delay": {"type": "int"}, + "master_refresh": {"type": "int"}, + "master_refresh_repeat": {"type": "int"}, + "master_repeat": {"type": "int"}, + }, + }, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "hello_source_address": {"type": "str"}, + "interface": {"type": "str"}, + "no_preempt": {"type": "bool", "default": False}, + "peer_address": {"type": "str"}, + "preempt_delay": {"type": "int"}, + "priority": {"type": "int"}, + "rfc3768_compatibility": {"type": "bool", "default": False}, + "track": { + "type": "dict", + "options": { + "exclude_vrrp_interface": {"type": "bool"}, + "interface": {"type": "list", "elements": "str"}, + }, + }, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + "vrid": {"type": "int", "required": False}, + }, + }, + "snmp": { + "type": "str", + "choices": ["disabled", "enabled"], + }, + "sync_groups": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "health_check": { + "type": "dict", + "options": { + "failure_count": {"type": "int"}, + "interval": {"type": "int"}, + "ping": {"type": "str"}, + "script": {"type": "str"}, + }, + }, + "member": {"type": "list", "elements": "str"}, + "transition_script": { + "type": "dict", + "options": { + "backup": {"type": "str"}, + "fault": {"type": "str"}, + "master": {"type": "str"}, + "stop": {"type": "str"}, + }, + }, + }, + }, + }, + }, + }, + }, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "purged", + "replaced", + "gathered", + "rendered", + "parsed", + "overridden", + ], + "default": "merged", + }, + "running_config": {"type": "str"}, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/hostname/hostname.py b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py index 12864e66..b1af7b9c 100644 --- a/plugins/module_utils/network/vyos/argspec/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/argspec/hostname/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py index 14b67c28..8ba119f3 100644 --- a/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/interfaces/interfaces.py @@ -4,31 +4,12 @@ # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -############################################# -# WARNING # -############################################# -# -# This file is auto generated by the resource -# module builder playbook. -# -# Do not edit this file manually. -# -# Changes to this file will be over written -# by the resource module builder. -# -# Changes should be made in the model used to -# generate this file or in the resource module -# builder template. -# -############################################# """ The arg spec for the vyos_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -79,6 +60,7 @@ class InterfacesArgs(object): # pylint: disable=R0903 }, "type": "list", }, + "vrf": {"type": "str"}, }, "type": "list", }, diff --git a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py index 4dee518e..d6d9e298 100644 --- a/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/l3_interfaces/l3_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_l3_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py index 956c4385..7744eea0 100644 --- a/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/lag_interfaces/lag_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lag_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py index d79de617..6d1129e8 100644 --- a/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/argspec/lldp_global/lldp_global.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lldp_global module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py index fd6c6271..4bb742f5 100644 --- a/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/lldp_interfaces/lldp_interfaces.py @@ -25,10 +25,8 @@ The arg spec for the vyos_lldp_interfaces module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py index 734d190e..f6800e9b 100644 --- a/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/argspec/logging_global/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/nat/__init__.py b/plugins/module_utils/network/vyos/argspec/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/nat/nat.py b/plugins/module_utils/network/vyos/argspec/nat/nat.py new file mode 100644 index 00000000..6f81c53c --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/nat/nat.py @@ -0,0 +1,652 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The arg spec for the vyos_nat module +""" + + +class NatArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_nat module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "nat": { + "type": "dict", + "options": { + "cgnat": { + "type": "dict", + "options": { + "log_allocation": { + "type": "bool", + }, + "pool": { + "type": "dict", + "options": { + "external": { + "type": "list", + "elements": "dict", + "options": { + "name": { + "type": "str", + "required": True, + }, + "external_port_range": { + "type": "str", + }, + "per_user_limit": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + }, + }, + "range": { + "type": "list", + "elements": "dict", + "options": { + "value": { + "type": "str", + "required": True, + }, + "seq": { + "type": "str", + }, + }, + }, + }, + }, + "internal": { + "type": "list", + "elements": "dict", + "options": { + "name": { + "type": "str", + "required": True, + }, + "range": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "source": { + "type": "dict", + "options": { + "pool": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "pool": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "protocol": { + "type": "str", + }, + "packet_type": { + "type": "str", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "disable": { + "type": "bool", + }, + "inbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + "group": { + "type": "str", + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "fqdn": { + "type": "str", + }, + "port": { + "type": "str", + }, + "address_group": { + "type": "str", + }, + "domain_group": { + "type": "str", + }, + "mac_group": { + "type": "str", + }, + "network_group": { + "type": "str", + }, + "port_group": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + "redirect_port": { + "type": "str", + }, + "address_mapping": { + "type": "str", + "choices": [ + "random", + "persistent", + ], + }, + "port_mapping": { + "type": "str", + "choices": [ + "random", + "none", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "protocol": { + "type": "str", + }, + "packet_type": { + "type": "str", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "disable": { + "type": "bool", + }, + "outbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + "group": { + "type": "str", + }, + }, + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "fqdn": { + "type": "str", + }, + "address_group": { + "type": "str", + }, + "domain_group": { + "type": "str", + }, + "mac_group": { + "type": "str", + }, + "network_group": { + "type": "str", + }, + "port_group": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "source": { + "type": "dict", + "options": { + "address": {"type": "str"}, + "fqdn": {"type": "str"}, + "port": {"type": "str"}, + "address_group": {"type": "str"}, + "domain_group": {"type": "str"}, + "mac_group": {"type": "str"}, + "network_group": {"type": "str"}, + "port_group": {"type": "str"}, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + "address_mapping": { + "type": "str", + "choices": [ + "random", + "persistent", + ], + }, + "port_mapping": { + "type": "str", + "choices": [ + "random", + "none", + ], + }, + }, + }, + "load_balance": { + "type": "dict", + "options": { + "backend": { + "type": "list", + "elements": "dict", + "options": { + "ip": {"type": "str"}, + "weight": {"type": "int"}, + }, + }, + "hash": { + "type": "list", + "elements": "str", + "choices": [ + "source-address", + "destination-address", + "source-port", + "destination-port", + "random", + ], + }, + }, + }, + }, + }, + }, + }, + "static": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + }, + }, + "inbound_interface": { + "type": "str", + }, + "log": { + "type": "bool", + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + "nat64": { + "type": "dict", + "options": { + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "disable": { + "type": "bool", + }, + "match": { + "type": "dict", + "options": { + "mark": { + "type": "int", + }, + }, + }, + "source": { + "type": "dict", + "options": { + "prefix": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "pool": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "address": { + "type": "str", + }, + "description": { + "type": "str", + }, + "disable": { + "type": "bool", + }, + "port": { + "type": "str", + }, + "protocol": { + "type": "str", + "choices": [ + "icmp", + "tcp", + "udp", + ], + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "nat66": { + "type": "dict", + "options": { + "destination": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "disable": { + "type": "bool", + }, + "exclude": { + "type": "bool", + }, + "inbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + }, + }, + "log": { + "type": "bool", + }, + "protocol": { + "type": "str", + }, + "source": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + "source": { + "type": "dict", + "options": { + "rule": { + "type": "list", + "elements": "dict", + "options": { + "id": { + "type": "int", + "required": True, + }, + "description": { + "type": "str", + }, + "destination": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + "prefix": { + "type": "str", + }, + }, + }, + "disable": { + "type": "bool", + }, + "exclude": { + "type": "bool", + }, + "log": { + "type": "bool", + }, + "outbound_interface": { + "type": "dict", + "options": { + "name": { + "type": "str", + }, + }, + }, + "protocol": { + "type": "str", + }, + "source": { + "type": "dict", + "options": { + "port": { + "type": "str", + }, + "prefix": { + "type": "str", + }, + }, + }, + "translation": { + "type": "dict", + "options": { + "address": { + "type": "str", + }, + "port": { + "type": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "overridden", + "replaced", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py index 6940fb7e..5e10d05b 100644 --- a/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/argspec/ntp_global/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py index 0b5814be..ced9f98e 100644 --- a/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/argspec/ospf_interfaces/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py index 0a422e89..a939652b 100644 --- a/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/argspec/ospfv2/ospfv2.py @@ -25,10 +25,8 @@ The arg spec for the vyos_ospfv2 module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py index a59606dd..77a17d79 100644 --- a/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/argspec/ospfv3/ospfv3.py @@ -25,10 +25,8 @@ The arg spec for the vyos_ospfv3 module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -47,6 +45,12 @@ class Ospfv3Args(object): # pylint: disable=R0903 "area_id": {"type": "str"}, "export_list": {"type": "str"}, "import_list": {"type": "str"}, + "interface": { + "aliases": ["interfaces"], + "type": "list", + "elements": "dict", + "options": {"name": {"type": "str"}}, + }, "range": { "elements": "dict", "options": { diff --git a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py index b01a3e82..c74400ba 100644 --- a/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/argspec/prefix_lists/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py index 196db0c7..58ffa3e5 100644 --- a/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/argspec/route_maps/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# @@ -74,6 +73,8 @@ class Route_mapsArgs(object): # pylint: disable=R0903 }, "extcommunity_rt": {"type": "str"}, "extcommunity_soo": {"type": "str"}, + "extcommunity_bandwidth": {"type": "str"}, + "extcommunity_bandwidth_non_transitive": {"type": "bool"}, "ip_next_hop": {"type": "str"}, "ipv6_next_hop": { "type": "dict", @@ -100,6 +101,7 @@ class Route_mapsArgs(object): # pylint: disable=R0903 "src": {"type": "str"}, "tag": {"type": "str"}, "weight": {"type": "str"}, + "table": {"type": "str"}, }, }, "match": { @@ -178,6 +180,23 @@ class Route_mapsArgs(object): # pylint: disable=R0903 "next_hop": {"type": "str"}, }, }, + "protocol": { + "type": "str", + "choices": [ + "babel", + "bgp", + "connected", + "isis", + "kernel", + "ospf", + "ospfv3", + "rip", + "ripng", + "static", + "table", + "vnc", + ], + }, "large_community_large_community_list": { "type": "str", }, diff --git a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py index b94c2639..53516c4f 100644 --- a/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/argspec/snmp_server/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type ############################################# diff --git a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py index 365df48a..d8a4f11a 100644 --- a/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/argspec/static_routes/static_routes.py @@ -25,10 +25,8 @@ The arg spec for the vyos_static_routes module """ - from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..75fc1ce5 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,206 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_vpn_ipsec module +""" + + +class Vpn_ipsecArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vpn_ipsec module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "ike_group": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "close_action": { + "type": "str", + "choices": ["none", "trap", "start"], + }, + "dead_peer_detection": { + "type": "dict", + "options": { + "action": { + "type": "str", + "choices": ["trap", "clear", "restart"], + }, + "interval": {"type": "int"}, + "timeout": {"type": "int"}, + }, + }, + "disable_mobike": {"type": "bool"}, + "ikev2_reauth": {"type": "bool"}, + "key_exchange": { + "type": "str", + "choices": ["ikev1", "ikev2"], + }, + "lifetime": {"type": "int"}, + "mode": {"type": "str", "choices": ["main", "aggressive"]}, + "proposal": { + "type": "list", + "elements": "dict", + "options": { + "proposal_id": {"type": "int"}, + "dh_group": {"type": "int"}, + "encryption": {"type": "str"}, + "hash": {"type": "str"}, + "prf": {"type": "str"}, + }, + }, + }, + }, + "esp_group": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "compression": {"type": "bool"}, + "disable_rekey": {"type": "bool"}, + "life_bytes": {"type": "int"}, + "life_packets": {"type": "int"}, + "lifetime": {"type": "int"}, + "mode": { + "type": "str", + "choices": ["tunnel", "transport"], + }, + "pfs": {"type": "str"}, + "proposal": { + "type": "list", + "elements": "dict", + "options": { + "proposal_id": {"type": "int"}, + "encryption": {"type": "str"}, + "hash": {"type": "str"}, + }, + }, + }, + }, + "authentication": { + "type": "dict", + "options": { + "psk": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "id": {"type": "list", "elements": "str"}, + "dhcp_interface": { + "type": "list", + "elements": "str", + }, + "secret": {"type": "str", "no_log": True}, + "secret_type": { + "type": "str", + "choices": ["base64", "hex", "plaintext"], + }, + }, + }, + "ppk": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "id": {"type": "list", "elements": "str"}, + "secret": {"type": "str", "no_log": True}, + "secret_type": { + "type": "str", + "choices": ["base64", "hex", "plaintext"], + }, + }, + }, + }, + }, + "profile": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "authentication": { + "type": "dict", + "options": { + "mode": { + "type": "str", + "choices": ["pre-shared-secret"], + }, + "pre_shared_secret": {"type": "str", "no_log": True}, + }, + }, + "bind_tunnel": {"type": "list", "elements": "str"}, + "disable": {"type": "bool"}, + "esp_group": {"type": "str"}, + "ike_group": {"type": "str"}, + }, + }, + "interface": {"type": "list", "elements": "str"}, + "log": { + "type": "dict", + "options": { + "level": {"type": "int"}, + "subsystem": { + "type": "list", + "elements": "str", + }, + }, + }, + "options": { + "type": "dict", + "options": { + "disable_route_autoinstall": {"type": "bool"}, + "flexvpn": {"type": "bool"}, + "interface": {"type": "str"}, + "retransmission": { + "type": "dict", + "options": { + "attempts": {"type": "int"}, + "base": {"type": "float"}, + "timeout": {"type": "int"}, + }, + }, + "virtual_ip": {"type": "bool"}, + }, + }, + "disable_uniqreqids": {"type": "bool"}, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..bd23d4d3 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_vpn_ipsec_s2s module +""" + + +class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vpn_ipsec_s2s module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "peer": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "disable": {"type": "bool"}, + "authentication": { + "type": "dict", + "options": { + "local_id": {"type": "str"}, + "ppk": { + "type": "dict", + "options": { + "id": {"type": "str"}, + "required": {"type": "bool"}, + }, + }, + "rsa": { + "type": "dict", + "options": { + "local_key": {"type": "str", "no_log": True}, + "passphrase": {"type": "str", "no_log": True}, + "remote_key": {"type": "str", "no_log": True}, + }, + }, + "x509": { + "type": "dict", + "options": { + "certificate": {"type": "str"}, + "passphrase": {"type": "str", "no_log": True}, + "ca_certificate": { + "type": "list", + "elements": "str", + }, + }, + }, + "mode": { + "type": "str", + "choices": [ + "pre-shared-secret", + "rsa", + "x509", + ], + }, + "remote_id": {"type": "str"}, + "use_x509_id": {"type": "bool"}, + }, + }, + "childless": { + "type": "str", + "choices": ["allow", "prefer", "force", "never"], + }, + "connection_type": { + "type": "str", + "choices": ["initiate", "trap", "none"], + }, + "default_esp_group": {"type": "str"}, + "description": {"type": "str"}, + "dhcp_interface": {"type": "str"}, + "force_udp_encapsulation": {"type": "bool"}, + "ike_group": {"type": "str"}, + "ikev2_reauth": { + "type": "str", + "choices": ["yes", "no", "inherit"], + }, + "local_address": {"type": "str"}, + "remote_address": {"type": "list", "elements": "str"}, + "replay_window": {"type": "int"}, + "tunnel": { + "type": "list", + "elements": "dict", + "options": { + "tunnel_id": {"type": "int", "required": True}, + "disable": {"type": "bool"}, + "esp_group": {"type": "str"}, + "local": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "protocol": {"type": "str"}, + "priority": {"type": "int"}, + "remote": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + "virtual_address": {"type": "list", "elements": "str"}, + "vti": { + "type": "dict", + "options": { + "bind": {"type": "str"}, + "esp_group": {"type": "str"}, + "traffic_selector": { + "type": "dict", + "options": { + "local": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "remote": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/argspec/vrf/__init__.py b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/argspec/vrf/vrf.py b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py new file mode 100644 index 00000000..20947f9d --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vrf/vrf.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_vrf module +""" + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp_global.bgp_global import ( + Bgp_globalArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv2.ospfv2 import ( + Ospfv2Args, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv3.ospfv3 import ( + Ospfv3Args, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) + + +class VrfArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vrf module""" + + bgp_argument_spec = Bgp_globalArgs.argument_spec["config"] + static_routes_argument_spec = Static_routesArgs.argument_spec["config"] + ospfv2_argument_spec = Ospfv2Args.argument_spec["config"] + ospfv3_argument_spec = Ospfv3Args.argument_spec["config"] + + argument_spec = { + "config": { + "type": "dict", + "options": { + "bind_to_all": {"type": "bool", "default": False}, + "instances": { + "type": "list", + "elements": "dict", + "options": { + "name": {"required": True, "type": "str"}, + "description": {"type": "str"}, + "disable": { + "aliases": ["disabled"], + "default": False, + "type": "bool", + }, + "table_id": {"type": "int"}, + "vni": {"type": "int"}, + "address_family": { + "type": "list", + "elements": "dict", + "options": { + "afi": { + "type": "str", + "choices": ["ipv4", "ipv6"], + }, + "disable_forwarding": {"type": "bool", "default": False}, + "nht_no_resolve_via_default": {"type": "bool", "default": False}, + "route_maps": { + "type": "list", + "elements": "dict", + "options": { + "rm_name": {"type": "str", "required": True}, + "protocol": { + "type": "str", + "choices": [ + "any", + "babel", + "bgp", + "eigrp", + "isis", + "ospf", + "rip", + "static", + ], + }, + }, + }, + }, + }, + "protocols": { + # "type": "list", # sanity + # "elements": "dict", + "type": "dict", + "options": { + "bgp": bgp_argument_spec, + "ospf": ospfv2_argument_spec, + "ospfv3": ospfv3_argument_spec, + "static": static_routes_argument_spec, + }, + }, + }, + }, + }, + }, + "state": { + "type": "str", + "choices": [ + "deleted", + "merged", + "replaced", + "overridden", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + "running_config": {"type": "str"}, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py index 0e6bec81..f1494698 100644 --- a/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/config/bgp_address_family/bgp_address_family.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -32,15 +30,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family import ( Bgp_address_familyTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( Bgp_address_familyTemplate14, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_address_family(ResourceModule): """ @@ -65,7 +62,7 @@ class Bgp_address_family(ResourceModule): self._tmplt = Bgp_address_familyTemplate() def parse(self): - """ override parse to check template """ + """override parse to check template""" self._validate_template() return super().parse() @@ -93,9 +90,11 @@ class Bgp_address_family(ResourceModule): wantd = {} haved = {} - if (self.want.get("as_number") == self.have.get("as_number") or - not self.have or - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")): + if ( + self.want.get("as_number") == self.have.get("as_number") + or not self.have + or LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): if self.want: wantd = {self.want["as_number"]: self.want} if self.have: @@ -113,16 +112,16 @@ class Bgp_address_family(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - for k, have in iteritems(haved): + for k, have in haved.items(): self._delete_af(wantd, have) wantd = {} if self.state == "overridden": - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -149,23 +148,23 @@ class Bgp_address_family(ResourceModule): def _compare_af(self, want, have): waf = want.get("address_family", {}) haf = have.get("address_family", {}) - for name, entry in iteritems(waf): + for name, entry in waf.items(): self._compare_lists( entry, have=haf.get(name, {}), as_number=want["as_number"], afi=name, ) - for name, entry in iteritems(haf): + for name, entry in haf.items(): if name not in waf.keys() and self.state == "replaced": continue self._compare_lists({}, entry, as_number=have["as_number"], afi=name) def _delete_af(self, want, have): - for as_num, entry in iteritems(want): - for afi, af_entry in iteritems(entry.get("address_family", {})): + for as_num, entry in want.items(): + for afi, af_entry in entry.get("address_family", {}).items(): if have.get("address_family"): - for hafi, hentry in iteritems(have["address_family"]): + for hafi, hentry in have["address_family"].items(): if hafi == afi: self.commands.append( self._tmplt.render( @@ -177,9 +176,9 @@ class Bgp_address_family(ResourceModule): True, ), ) - for neigh, neigh_entry in iteritems(entry.get("neighbors", {})): + for neigh, neigh_entry in entry.get("neighbors", {}).items(): if have.get("neighbors"): - for hneigh, hnentry in iteritems(have["neighbors"]): + for hneigh, hnentry in have["neighbors"].items(): if hneigh == neigh: if not neigh_entry.get("address_family"): self.commands.append( @@ -239,9 +238,9 @@ class Bgp_address_family(ResourceModule): ] wneigh = want.get("neighbors", {}) hneigh = have.get("neighbors", {}) - for name, entry in iteritems(wneigh): - for afi, af_entry in iteritems(entry.get("address_family")): - for k, val in iteritems(af_entry): + for name, entry in wneigh.items(): + for afi, af_entry in entry.get("address_family").items(): + for k, val in af_entry.items(): w = { "as_number": want["as_number"], "neighbors": { @@ -268,7 +267,7 @@ class Bgp_address_family(ResourceModule): want=w, have=h, ) - for name, entry in iteritems(hneigh): + for name, entry in hneigh.items(): if name not in wneigh.keys(): # remove surplus config for overridden and replaced if self.state != "replaced": @@ -284,9 +283,9 @@ class Bgp_address_family(ResourceModule): ) continue - for hafi, haf_entry in iteritems(entry.get("address_family")): + for hafi, haf_entry in entry.get("address_family").items(): # remove surplus configs for given neighbor - replace and overridden - for k, val in iteritems(haf_entry): + for k, val in haf_entry.items(): h = { "as_number": have["as_number"], "neighbors": { @@ -317,7 +316,7 @@ class Bgp_address_family(ResourceModule): for attrib in ["redistribute", "networks", "aggregate_address"]: wdict = want.pop(attrib, {}) hdict = have.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): if entry != hdict.get(key, {}): self.compare( parsers=parsers, @@ -348,7 +347,7 @@ class Bgp_address_family(ResourceModule): + attrib, ) hdict = {} - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): self.compare( parsers=parsers, want={}, @@ -358,7 +357,7 @@ class Bgp_address_family(ResourceModule): }, ) # de-duplicate child commands if parent command is present - for val in (self.commands): + for val in self.commands: for val2 in self.commands: if val != val2 and val2.startswith(val): self.commands.remove(val2) @@ -366,13 +365,11 @@ class Bgp_address_family(ResourceModule): def _compare_asn(self, want, have): if want.get("as_number") and not have.get("as_number"): self.commands.append( - "set protocols bgp " - + "system-as " - + str(want.get("as_number")), + "set protocols bgp " + "system-as " + str(want.get("as_number")), ) def _bgp_af_list_to_dict(self, entry): - for name, proc in iteritems(entry): + for name, proc in entry.items(): if "address_family" in proc: af_dict = {} for entry in proc.get("address_family"): diff --git a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py index 91a5af12..2d7d3e63 100644 --- a/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/config/bgp_global/bgp_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -18,7 +17,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -118,10 +116,10 @@ class Bgp_global(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "purged": h_del = {} - for k, v in iteritems(haved): + for k, v in haved.items(): if k in wantd or not wantd: h_del.update({k: v}) - for num, entry in iteritems(h_del): + for num, entry in h_del.items(): self.commands.append(self._tmplt.render({"as_number": num}, "router", True)) wantd = {} @@ -129,7 +127,7 @@ class Bgp_global(ResourceModule): self._compare(want={}, have=self.have) wantd = {} - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -144,7 +142,7 @@ class Bgp_global(ResourceModule): parsers = ["maximum_paths", "timers"] self._compare_neighbor(want, have) self._compare_bgp_params(want, have) - for name, entry in iteritems(want): + for name, entry in want.items(): if name != "as_number": self.compare( parsers=parsers, @@ -154,7 +152,7 @@ class Bgp_global(ResourceModule): name: have.pop(name, {}), }, ) - for name, entry in iteritems(have): + for name, entry in have.items(): if name != "as_number": self.compare( parsers=parsers, @@ -217,7 +215,7 @@ class Bgp_global(ResourceModule): hneigh = have.pop("neighbor", {}) self._compare_neigh_lists(wneigh, hneigh) - for name, entry in iteritems(wneigh): + for name, entry in wneigh.items(): for k, v in entry.items(): if k == "address": continue @@ -233,7 +231,7 @@ class Bgp_global(ResourceModule): }, have={"as_number": want["as_number"], "neighbor": h}, ) - for name, entry in iteritems(hneigh): + for name, entry in hneigh.items(): if name not in wneigh.keys(): if self._check_af(name): msg = "Use the _bgp_address_family module to delete the address_family under neighbor {0}, before replacing/deleting the neighbor.".format( @@ -281,7 +279,7 @@ class Bgp_global(ResourceModule): wbgp = want.pop("bgp_params", {}) hbgp = have.pop("bgp_params", {}) - for name, entry in iteritems(wbgp): + for name, entry in wbgp.items(): if name == "confederation": if entry != hbgp.pop(name, {}): self.addcmd( @@ -325,7 +323,7 @@ class Bgp_global(ResourceModule): if not wbgp and hbgp: self.commands.append("delete protocols bgp" + self._asn_mod + " parameters") hbgp = {} - for name, entry in iteritems(hbgp): + for name, entry in hbgp.items(): if name == "confederation": self.commands.append( "delete protocols bgp" + self._asn_mod + " parameters confederation", @@ -362,7 +360,7 @@ class Bgp_global(ResourceModule): ]: wdict = want.pop(attrib, {}) hdict = have.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): if entry != hdict.pop(key, {}): self.addcmd(entry, "neighbor.{0}".format(attrib), False) # remove remaining items in have for replaced @@ -370,7 +368,7 @@ class Bgp_global(ResourceModule): self.addcmd(entry, "neighbor.{0}".format(attrib), True) def _bgp_global_list_to_dict(self, entry): - for name, proc in iteritems(entry): + for name, proc in entry.items(): if "neighbor" in proc: neigh_dict = {} for entry in proc.get("neighbor", []): diff --git a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py index 34dc0ed6..289037ef 100644 --- a/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/config/firewall_global/firewall_global.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -28,12 +27,16 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.u from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( + in_target_not_none, list_diff_want_only, ) - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( + get_os_version, + load_config, +) class Firewall_global(ConfigBase): @@ -74,6 +77,13 @@ class Firewall_global(ConfigBase): warnings = list() commands = list() + try: + self._module.params["comment"] + except KeyError: + comment = [] + else: + comment = self._module.params["comment"] + if self.state in self.ACTION_STATES: existing_firewall_global_facts = self.get_firewall_global_facts() else: @@ -82,6 +92,12 @@ class Firewall_global(ConfigBase): if self.state in self.ACTION_STATES or self.state == "rendered": commands.extend(self.set_config(existing_firewall_global_facts)) + if commands and self._module._diff: + commit = not self._module.check_mode + diff = load_config(self._module, commands, commit=commit, comment=comment) + if diff: + result["diff"] = {"prepared": str(diff)} + if commands and self.state in self.ACTION_STATES: if not self._module.check_mode: self._connection.edit_config(commands) @@ -189,7 +205,7 @@ class Firewall_global(ConfigBase): "twa_hazards_protection", ) if want: - for key, val in iteritems(want): + for key, val in want.items(): if val and key in b_set and not have: commands.append(self._form_attr_cmd(attr=key, opr=False)) elif val and key in b_set and have and key in have and have[key] != val: @@ -199,7 +215,7 @@ class Firewall_global(ConfigBase): elif not want and have: commands.append(self._compute_command(opr=False)) elif have: - for key, val in iteritems(have): + for key, val in have.items(): if val and key in b_set: commands.append(self._form_attr_cmd(attr=key, opr=False)) else: @@ -225,6 +241,8 @@ class Firewall_global(ConfigBase): commands.extend(self._render_state_policy(key, w, h, opr=opr)) elif key == "route_redirects": commands.extend(self._render_route_redirects(key, w, h, opr=opr)) + elif key == "zone": + commands.extend(self._render_zone(key, w, h, opr=opr)) return commands def _add_global_attr(self, w, h, opr=True): @@ -246,7 +264,7 @@ class Firewall_global(ConfigBase): "twa_hazards_protection", ) if w_fg: - for key, val in iteritems(w_fg): + for key, val in w_fg.items(): if opr and key in l_set and not (h and self._is_w_same(w_fg, h, key)): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), @@ -257,11 +275,7 @@ class Firewall_global(ConfigBase): self._form_attr_cmd(attr=key, key=self._bool_to_str(val), opr=opr), ) continue - if ( - key in l_set - and not self._in_target(h, key) - and not self._is_del(l_set, h) - ): + if key in l_set and not self._in_target(h, key) and not self._is_del(l_set, h): commands.append( self._form_attr_cmd(attr=key, val=self._bool_to_str(val), opr=opr), ) @@ -284,13 +298,13 @@ class Firewall_global(ConfigBase): if h: h_ping = h.get(attr) or {} if self._is_root_del(w[attr], h_ping, attr): - for item, value in iteritems(h[attr]): + for item, value in h[attr].items(): if not opr and item in l_set: commands.append(self._form_attr_cmd(attr=item, opr=opr)) elif w[attr]: if h and attr in h.keys(): h_ping = h.get(attr) or {} - for item, value in iteritems(w[attr]): + for item, value in w[attr].items(): if ( opr and item in l_set @@ -354,7 +368,7 @@ class Firewall_global(ConfigBase): cmd = self._compute_command(key="group", attr="ipv6-" + attr, opr=opr) else: cmd = self._compute_command(key="group", attr=attr, opr=opr) - for key, val in iteritems(want): + for key, val in want.items(): if val: if opr and key in l_set and not (h and self._is_w_same(want, h, key)): if key == "name": @@ -372,12 +386,18 @@ class Firewall_global(ConfigBase): ) elif not opr and key in l_set: if key == "name" and self._is_grp_del(h, want, key): + if len(commands) > 0 and commands[-1] == cmd + " " + want[ + "name" + ] + " " + self._grp_type( + attr, + ): + commands.pop() commands.append(cmd + " " + want["name"]) continue - if not (h and self._in_target(h, key)) and not self._is_grp_del( + if not (h and in_target_not_none(h, key)) and not self._is_grp_del( h, want, - key, + "name", ): commands.append(cmd + " " + want["name"] + " " + key) elif key == "members": @@ -438,6 +458,10 @@ class Firewall_global(ConfigBase): + " " + member[self._get_mem_type(type)], ) + elif not opr and not have: + commands.append( + cmd + " " + name + " " + self._grp_type(type), + ) return commands def _get_mem_type(self, group): @@ -473,10 +497,12 @@ class Firewall_global(ConfigBase): if want: for w in want: h = self.search_attrib_in_have(have, w, "connection_type") - for key, val in iteritems(w): + for key, val in w.items(): if val and key != "connection_type": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): - if key == "log" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if key == "log" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): commands.append( self._form_attr_cmd( key=attr + " " + w["connection_type"], @@ -502,7 +528,9 @@ class Firewall_global(ConfigBase): ), ) break # delete the whole thing and move on - if (not self._in_target(h, key) or h[key] is None) and (self._in_target(w, key) and w[key]): + if (not self._in_target(h, key) or h[key] is None) and ( + self._in_target(w, key) and w[key] + ): # delete if not being replaced and value currently exists commands.append( self._form_attr_cmd( @@ -534,23 +562,23 @@ class Firewall_global(ConfigBase): if want: for w in want: h = self.search_attrib_in_have(have, w, "afi") - if 'afi' in w: - afi = w['afi'] + if "afi" in w: + afi = w["afi"] else: - if h and 'afi' in h: - afi = h['afi'] + if h and "afi" in h: + afi = h["afi"] else: afi = None afi = None - for key, val in iteritems(w): - if val and key != "afi": + for key, val in w.items(): + if val is not None and key != "afi": if opr and key in l_set and not (h and self._is_w_same(w, h, key)): commands.append( self._form_attr_cmd( attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) elif not opr and key in l_set: @@ -560,7 +588,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) continue @@ -570,7 +598,7 @@ class Firewall_global(ConfigBase): attr=key, val=self._bool_to_str(val), opr=opr, - type=afi + type=afi, ), ) elif key == "icmp_redirects": @@ -590,20 +618,25 @@ class Firewall_global(ConfigBase): commands = [] h_red = {} l_set = ("send", "receive") - if w and 'afi' in w: - afi = w['afi'] + if w and "afi" in w: + afi = w["afi"] else: - if h and 'afi' in h: - afi = h['afi'] + if h and "afi" in h: + afi = h["afi"] else: afi = None if w[attr]: if h and attr in h.keys(): h_red = h.get(attr) or {} - for item, value in iteritems(w[attr]): + for item, value in w[attr].items(): if opr and item in l_set and not (h_red and self._is_w_same(w[attr], h_red, item)): commands.append( - self._form_attr_cmd(attr=item, val=self._bool_to_str(value), opr=opr, type=afi) + self._form_attr_cmd( + attr=item, + val=self._bool_to_str(value), + opr=opr, + type=afi, + ), ) elif ( not opr @@ -637,7 +670,12 @@ class Firewall_global(ConfigBase): :param type: AF type of attribute. :return: generated command. """ - command = self._compute_command(key=key, attr=self._map_attrib(attr, type=type), val=val, opr=opr) + command = self._compute_command( + key=key, + attr=self._map_attrib(attr, type=type), + val=val, + opr=opr, + ) return command def _compute_command(self, key=None, attr=None, val=None, remove=False, opr=True): @@ -654,14 +692,20 @@ class Firewall_global(ConfigBase): cmd = "delete firewall " else: cmd = "set firewall " - if attr and key != "group" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if ( + attr + and key not in ["group", "zone"] + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): cmd += "global-options " if key: cmd += key.replace("_", "-") + " " if attr: cmd += attr.replace("_", "-") if val and opr: - if key == "state_policy" and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if key == "state_policy" and LooseVersion(get_os_version(self._module)) >= LooseVersion( + "1.4", + ): cmd += "" else: cmd += " '" + str(val) + "'" @@ -768,3 +812,324 @@ class Firewall_global(ConfigBase): elif attrib == "validation": regex = "source-validation" return regex + + def _render_zone(self, attr, w, h, opr): + """ + This function forms the commands for group list/members attributes based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param opr: True/False. + :return: generated list of commands. + """ + commands = [] + h_grp = [] + w_grp = [] + l_set = ("name", "description", "default_action", "default_log", "local_zone") + if w: + w_grp = w.get(attr) or [] + if h: + h_grp = h.get(attr) or [] + + if w_grp: + for want in w_grp: + h = self.search_attrib_in_have(h_grp, want, "name") + + cmd = self._compute_command(key="zone", attr="", opr=opr) + + if not opr and self._is_grp_del(h, want, "name"): + commands.append(cmd + " " + want["name"]) + continue + + for key, val in want.items(): + if val: + if opr and key in l_set and not (h and self._is_w_same(want, h, key)): + if key == "name": + pass + elif isinstance(val, bool): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + else: + commands.append( + cmd + + " " + + want["name"] + + " " + + key.replace("_", "-") + + " '" + + str(want[key]) + + "'", + ) + elif not opr and key in l_set: + if not (h and in_target_not_none(h, key)) and not self._is_grp_del( + h, + want, + "name", + ): + commands.append( + cmd + " " + want["name"] + " " + key.replace("_", "-"), + ) + elif key == "interfaces": + commands.extend( + self._render_interfaces( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "intra_zone_filtering": + commands.extend( + self._render_izf( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + elif key == "sources": + commands.extend( + self._render_sources( + key, + want, + h, + opr, + cmd, + want["name"], + attr, + ), + ) + return commands + + def _render_interfaces(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for interfaces + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + # VyOS 1.5.0 GA moved 'interface' under a new 'member' node + # ("set firewall zone <name> member interface <ifname>"). 1.4.x and + # 1.5-rolling snapshots predating this change still use the bare + # 'interface' node. Known limitation: a 1.5-rolling build reporting + # "1.5" that predates this change will incorrectly get the new + # syntax -- accepted trade-off, see PR notes. + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"): + iface_kw = "member interface" + else: + iface_kw = "interface" + + if want: + if opr: + interfaces = list_diff_want_only(want, have) + + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and have: + interfaces = list_diff_want_only(want, have) + for interface in interfaces: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + elif not opr and not have: + for interface in want: + commands.append( + cmd + " " + name + " " + iface_kw + " " + interface, + ) + else: + self._module.fail_json(msg={"want": want, "have": have, "opr": opr}) + + return commands + + def _render_izf(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for intra zone filtering + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + if want: + if opr: + izfs = self._dict_diff(want, have) + for izf in izfs: + commands.append( + cmd + + " " + + name + + " intra-zone-filtering " + + izf[0].replace(".", " ") + + " " + + izf[1], + ) + elif not opr and have: + izfs = self._dict_diff(want, have) + + for izf in izfs: + commands.append( + cmd + " " + name + " intra-zone-filtering " + izf[0].replace(".", " "), + ) + elif not opr and not have: + commands.append( + cmd + " " + name + " intra-zone-filtering", + ) + return commands + + def _dict_diff(self, want, have, path=""): + """ + Recursively find keys/values in `want` that differ or are missing in `have`. + Returns list of tuples: (full_path, value_in_want) + """ + diffs = [] + + have = have or {} + + for key, want_val in want.items(): + current_path = f"{path}.{key.replace('_', '-')}" if path else key.replace("_", "-") + + if key not in have: + if isinstance(want_val, dict): + diffs.extend(self._dict_diff(want_val, {}, current_path)) + elif isinstance(want_val, list): + for i, item in enumerate(want_val): + if isinstance(item, dict): + diffs.extend(self._dict_diff(item, {}, f"{current_path}[{i}]")) + else: + diffs.append((f"{current_path}[{i}]", item)) + else: + diffs.append((current_path, want_val)) + + else: + have_val = have[key] + + if isinstance(want_val, dict) and isinstance(have_val, dict): + diffs.extend(self._dict_diff(want_val, have_val, current_path)) + + elif isinstance(want_val, list) and isinstance(have_val, list): + for i, item in enumerate(want_val): + if i >= len(have_val): + diffs.append((f"{current_path}[{i}]", item)) + elif isinstance(item, dict) and isinstance(have_val[i], dict): + diffs.extend( + self._dict_diff(item, have_val[i], f"{current_path}[{i}]"), + ) + elif item != have_val[i]: + diffs.append((f"{current_path}[{i}]", item)) + + elif want_val != have_val: + diffs.append((current_path, want_val)) + + return diffs + + def _render_sources(self, attr, w, h, opr, cmd, name, type): + """ + This function forms the commands for sources (from) + based on the 'opr'. + :param attr: attribute name. + :param w: the desired config. + :param h: the target config. + :param cmd: commands to be prepend. + :param name: name of group. + :param type: group type. + :return: generated list of commands. + """ + commands = [] + have = [] + if w: + want = w.get(attr) or [] + if h: + have = h.get(attr) or [] + + have_index = {item["zone"]: item for item in have} + + for item1 in want: + zone = item1["zone"] + + if zone in have_index: + item2 = have_index[zone] + + wfw = item1.get("firewall", {}) + hfw = item2.get("firewall", {}) + if wfw: + if opr: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-") + + " " + + source[1], + ) + elif not opr and hfw: + sources = self._dict_diff(wfw, hfw) + for source in sources: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + source[0].replace("_", "-"), + ) + elif not opr and not hfw: + commands.append( + cmd + " " + name + " from " + zone, + ) + elif opr: + wfw = item1.get("firewall", {}) + for key, val in wfw.items(): + if val: + commands.append( + cmd + + " " + + name + + " from " + + zone + + " firewall " + + key.replace("_", "-") + + " " + + val, + ) + elif not opr: + commands.append( + cmd + " " + name + " from " + zone, + ) + return commands diff --git a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py index 85a8042f..ec1aaef1 100644 --- a/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py +++ b/plugins/module_utils/network/vyos/config/firewall_interfaces/firewall_interfaces.py @@ -10,8 +10,8 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py index 2942b191..be36d3fe 100644 --- a/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/config/firewall_rules/firewall_rules.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -30,10 +29,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( list_diff_want_only, ) - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version - -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( + get_os_version, + load_config, +) class Firewall_rules(ConfigBase): @@ -78,6 +80,14 @@ class Firewall_rules(ConfigBase): result = {"changed": False} warnings = list() commands = list() + diff = None + + try: + self._module.params["comment"] + except KeyError: + comment = [] + else: + comment = self._module.params["comment"] if self.state in self.ACTION_STATES: existing_firewall_rules_facts = self.get_firewall_rules_facts() @@ -87,6 +97,12 @@ class Firewall_rules(ConfigBase): if self.state in self.ACTION_STATES or self.state == "rendered": commands.extend(self.set_config(deepcopy(existing_firewall_rules_facts))) + if commands and self._module._diff: + commit = not self._module.check_mode + diff = load_config(self._module, commands, commit=commit, comment=comment) + if diff: + result["diff"] = {"prepared": str(diff)} + if commands and self.state in self.ACTION_STATES: if not self._module.check_mode: self._connection.edit_config(commands) @@ -213,13 +229,17 @@ class Firewall_rules(ConfigBase): commands.append(self._compute_command(rs_id, remove=True)) # Blank out the only rule set that it is removed. for entry in have: - if entry['afi'] == rs_id['afi'] and rs_id['name']: + if entry["afi"] == rs_id["afi"] and rs_id["name"]: entry["rule_sets"] = [ - rule_set for rule_set in entry["rule_sets"] if rule_set.get("name") != rs_id['name'] + rule_set + for rule_set in entry["rule_sets"] + if rule_set.get("name") != rs_id["name"] ] - elif entry['afi'] == rs_id['afi'] and rs_id['filter']: + elif entry["afi"] == rs_id["afi"] and rs_id["filter"]: entry["rule_sets"] = [ - rule_set for rule_set in entry["rule_sets"] if rule_set.get("filter") != rs_id['filter'] + rule_set + for rule_set in entry["rule_sets"] + if rule_set.get("filter") != rs_id["filter"] ] commands.extend(self._state_merged(want, have)) return commands @@ -264,7 +284,7 @@ class Firewall_rules(ConfigBase): for h in have: if h["afi"] == w["afi"]: commands.append( - self._compute_command(self._rs_id(None, w["afi"]), remove=True) + self._compute_command(self._rs_id(None, w["afi"]), remove=True), ) elif have: for h in have: @@ -294,7 +314,7 @@ class Firewall_rules(ConfigBase): h_rs = deepcopy(remove_empties(have)) h_rules = h_rs.pop("rules", None) if w_rs: - for key, val in iteritems(w_rs): + for key, val in w_rs.items(): if opr and key in l_set and not (h_rs and self._is_w_same(w_rs, h_rs, key)): if key == "enable_default_log": if val and (not h_rs or key not in h_rs or not h_rs[key]): @@ -338,6 +358,7 @@ class Firewall_rules(ConfigBase): "disable", "description", "jump_target", + "offload_target", ) if w_rules: for w in w_rules: @@ -345,7 +366,7 @@ class Firewall_rules(ConfigBase): h = self.search_rules_in_have_rs(h_rules, w["number"]) if w != h and self.state == "replaced": h = {} - for key, val in iteritems(w): + for key, val in w.items(): if val: if opr and key in l_set and not (h and self._is_w_same(w, h, key)): if key == "disable": @@ -443,7 +464,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_state = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -452,7 +473,9 @@ class Firewall_rules(ConfigBase): if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): commands.append(cmd + (" " + attr + " " + item)) else: - commands.append(cmd + (" " + attr + " " + item + " " + self._bool_to_str(val))) + commands.append( + cmd + (" " + attr + " " + item + " " + self._bool_to_str(val)), + ) elif not opr and item in l_set and not self._in_target(h_state, item): commands.append(cmd + (" " + attr + " " + item)) return commands @@ -504,7 +527,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_recent = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -532,7 +555,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_icmp = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -555,7 +578,9 @@ class Firewall_rules(ConfigBase): else: commands.append(cmd + (" " + attr + " " + item + " " + str(val))) elif not opr and item in l_set and not self._in_target(h_icmp, item): - commands.append(cmd + (" " + attr + " " + item.replace("_", "-") + " " + str(val))) + commands.append( + cmd + (" " + attr + " " + item.replace("_", "-") + " " + str(val)), + ) return commands def _add_interface(self, attr, w, h, cmd, opr): @@ -573,15 +598,15 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_if = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if opr and item in l_set and not (h_if and self._is_w_same(w[attr], h_if, item)): commands.append( cmd - + (" " + attr.replace("_", "-") + " " + item.replace("_", "-") + " " + val) + + (" " + attr.replace("_", "-") + " " + item.replace("_", "-") + " " + val), ) elif not opr and item in l_set and not (h_if and self._in_target(h_if, item)): commands.append( - cmd + (" " + attr.replace("_", "-") + " " + item.replace("_", "-")) + cmd + (" " + attr.replace("_", "-") + " " + item.replace("_", "-")), ) return commands @@ -608,7 +633,7 @@ class Firewall_rules(ConfigBase): if w[attr]: if h and attr in h.keys(): h_time = h.get(attr) or {} - for item, val in iteritems(w[attr]): + for item, val in w[attr].items(): if ( opr and item in l_set @@ -654,14 +679,14 @@ class Firewall_rules(ConfigBase): for flag in flags: invert = flag.get("invert", False) commands.append( - cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]), ) elif not opr: flags = list_diff_want_only(want, have) for flag in flags: invert = flag.get("invert", False) commands.append( - cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]) + cmd + (" " + attr + " flags " + ("not " if invert else "") + flag["flag"]), ) return commands @@ -837,7 +862,7 @@ class Firewall_rules(ConfigBase): h_group = {} if h and h.get(attr) and key in h[attr].keys(): h_group = h[attr].get(key) - for item, val in iteritems(group): + for item, val in group.items(): if val: if ( opr @@ -969,7 +994,10 @@ class Firewall_rules(ConfigBase): if number: cmd += " rule " + str(number) if attrib: - if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") and attrib == "enable_default_log": + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attrib == "enable_default_log" + ): cmd += " " + "default-log" else: cmd += " " + attrib.replace("_", "-") @@ -1107,15 +1135,23 @@ class Firewall_rules(ConfigBase): for item in rs: self._prune_stubs(item) elif isinstance(rs, dict): - keys_to_remove = [key for key, value in rs.items() - if ( - (key == "disable" and value is False) - or - (key == "log" and value == "disable" and - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")) - or - (key in ["new", "invalid", "related", "established"] and value is False and - LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4")))] + keys_to_remove = [ + key + for key, value in rs.items() + if ( + (key == "disable" and value is False) + or ( + key == "log" + and value == "disable" + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ) + or ( + key in ["new", "invalid", "related", "established"] + and value is False + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ) + ) + ] for key in keys_to_remove: del rs[key] for key in rs: @@ -1131,10 +1167,20 @@ class Firewall_rules(ConfigBase): return True elif isinstance(w, list) and isinstance(rs, list): try: - sorted_list1 = sorted(w, key=lambda x: str(x)) # pylint: disable=unnecessary-lambda - sorted_list2 = sorted(rs, key=lambda x: str(x)) # pylint: disable=unnecessary-lambda + + def comparison(x): + if "name" in x: + return x["name"] + if "number" in x: + return x["number"] + return str(x) + + sorted_list1 = sorted(w, key=comparison) + sorted_list2 = sorted(rs, key=comparison) except TypeError: return False + if len(sorted_list1) != len(sorted_list2): + return False return all(self._is_same_rs(x, y) for x, y in zip(sorted_list1, sorted_list2)) else: return w == rs diff --git a/plugins/module_utils/network/vyos/config/ha/__init__.py b/plugins/module_utils/network/vyos/config/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/config/ha/ha.py b/plugins/module_utils/network/vyos/config/ha/ha.py new file mode 100644 index 00000000..7fc96a35 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/ha/ha.py @@ -0,0 +1,705 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos_ha config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_empties, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import ( + HaTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + + +class Ha(ResourceModule): + """ + The vyos_ha config class + """ + + def __init__(self, module): + super(Ha, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="ha", + tmplt=HaTemplate(), + ) + self.parsers = [ + "disable", + ] + + self._validate_template() + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = HaTemplate() + else: + self._module.fail_json(msg="High Availability is not supported in this version of VyOS") + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + + if self.state not in ["parsed", "gathered", "purged"]: + self.generate_commands() + self.run_commands() + + if self.state == "purged": + wantd = {"disable": False} + haved = deepcopy(self.have) + if wantd != haved: + self.commands = ["delete high-availability"] + self.run_commands() + if "before" in self.result: + self._normalize_lists(self.result["before"]) + if "after" in self.result: + self._normalize_lists(self.result["after"]) + if "parsed" in self.result: + self._normalize_lists(self.result["parsed"]) + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + for entry in wantd, haved: + self._list_to_named_dict(entry) + self._normalize_lists(entry) + + if self.state in ["deleted"]: + wantd, haved, p = self._prune_stubs(self._module.params.get("config", {}), haved) + + if self.state in ["overridden"]: + wo = deepcopy(wantd) + self._diff_w_h(wo, haved) + + haved_disable = haved.get("disable") + + for k1, v1 in wo.items(): + + if not isinstance(v1, dict): + continue + + for name, obj in v1.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {name: {}}}, haved) + haved = hi + + for k2, v2 in v1.items(): + if not isinstance(v2, dict): + continue + + for name, obj in v2.items(): + if isinstance(obj, dict) and not obj: + wi, hi, pi = self._prune_stubs({k1: {k2: {name: {}}}}, haved) + haved = hi + + if haved_disable is not None: + haved["disable"] = haved_disable + + keys = set(wantd) | set(haved) + + for k in keys: + + want = wantd.get(k, {}) + have = haved.get(k, {}) + + if k == "vrrp": + if self.state in ["merged"]: + want = combine(have, want, recursive=True, list_merge="append_rp") + self._compare_vrrp(want, have) + + if k == "virtual_servers": + if self.state in ["merged"]: + want = combine(have, want, recursive=True) + self._compare_vsrvs(want, have) + + if self.state in ["deleted"] and k == "disable": + want = have + if self.state in ["overridden"] and k == "disable" and not want: + want = False + if self.state in ["rendered"]: + have = None + + self.compare( + parsers=self.parsers, + want={k: want}, + have={k: have}, + ) + + self.commands = list(dict.fromkeys(self.commands)) + + def _compare_vsrvs(self, want, have): + """Compare virtual servers. + + Pre-index both want and have by (name, attribute) signature so that + each lookup is O(1) instead of O(n). Groups that are identical + between want and have are skipped entirely via an equality + short-circuit before leaf decomposition. + """ + vs_parsers = [ + "virtual_servers.address", + "virtual_servers.algorithm", + "virtual_servers.delay_loop", + "virtual_servers.forward_method", + "virtual_servers.persistence_timeout", + "virtual_servers.fwmark", + "virtual_servers.port", + "virtual_servers.protocol", + "virtual_servers.real_server.port", + "virtual_servers.real_server.health_check_script", + "virtual_servers.real_server.connection_timeout", + ] + + want_index = ( + {vs["name"]: vs for vs in want.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(want, dict) + else {} + ) + have_index = ( + {vs["name"]: vs for vs in have.values() if isinstance(vs, dict) and vs.get("name")} + if isinstance(have, dict) + else {} + ) + + all_names = set(want_index) | set(have_index) + + for name in all_names: + w = want_index.get(name, {}) + h = have_index.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_named_leafs(w) if w else [] + hlist = self._extract_named_leafs(h) if h else [] + + if self.state == "rendered": + hlist = [] + + def _vsrv_sig(item): + if not isinstance(item, dict): + return None + iname = item.get("name") + if not iname: + return None + if "real_server" in item: + rs = item["real_server"] + if not isinstance(rs, dict) or "address" not in rs: + return None + addr = rs["address"] + for k in rs: + if k != "address": + return ("real_server", iname, addr, k) + return ("real_server", iname, addr, None) + for k in item: + if k != "name": + return ("attr", iname, k) + return None + + have_leaf_index = {} + for hdict in hlist: + sig = _vsrv_sig(hdict) + if sig is not None: + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = _vsrv_sig(wdict) + if sig is not None: + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vs_parsers, + want={"virtual_servers": wdict}, + have={"virtual_servers": hdict}, + ) + + def _compare_vrrp(self, want, have): + """Compare VRRP groups and sync-groups. + + Pre-index groups by name so matching is O(1). Groups that are + identical between want and have are skipped via equality + short-circuit before any leaf decomposition occurs — this is the + dominant performance win for large idempotent runs. + """ + vrrp_parsers = [ + "vrrp.snmp", + "vrrp.global_parameters", + "vrrp.global_parameters.garp", + "vrrp.groups", + "vrrp.groups.disable", + "vrrp.groups.no_preempt", + "vrrp.groups.rfc3768_compatibility", + "vrrp.groups.address", + "vrrp.groups.excluded_address", + "vrrp.groups.garp", + "vrrp.groups.authentication", + "vrrp.groups.transition_script", + "vrrp.groups.health_check", + "vrrp.groups.track.interface", + "vrrp.groups.track.exclude_vrrp_interface", + "vrrp.sync_groups.member", + "vrrp.sync_groups.transition_script", + "vrrp.sync_groups.health_check", + ] + + if ( + have.get("snmp") == "enabled" + and want.get("snmp") != "enabled" + and self.state not in ["deleted", "overridden"] + and (self.state != "merged" or "snmp" in want) + ): + self.commands.append("delete high-availability vrrp snmp") + + non_named = {k: v for k, v in (want or {}).items() if k not in ("groups", "sync_groups")} + non_named_have = { + k: v for k, v in (have or {}).items() if k not in ("groups", "sync_groups") + } + + hlist_non = self._extract_leaf_items(non_named_have) + wlist_non = self._extract_leaf_items(non_named) + + if self.state == "rendered": + hlist_non = [] + + have_non_index = {} + for hdict in hlist_non: + sig = self._vrrp_leaf_sig(hdict) + have_non_index[sig] = hdict + + want_non_index = {} + for wdict in wlist_non: + sig = self._vrrp_leaf_sig(wdict) + want_non_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_non_index.items(): + wdict = want_non_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_non_index.items(): + hdict = have_non_index.get(sig, {}) + self.compare(parsers=vrrp_parsers, want={"vrrp": wdict}, have={"vrrp": hdict}) + + for section in ("groups", "sync_groups"): + want_objs = (want or {}).get(section, {}) + have_objs = (have or {}).get(section, {}) + + if not isinstance(want_objs, dict): + want_objs = {} + if not isinstance(have_objs, dict): + have_objs = {} + + all_names = set(want_objs) | set(have_objs) + + for name in all_names: + w = want_objs.get(name, {}) + h = have_objs.get(name, {}) + + if w == h and self.state not in ["rendered"]: + continue + + wlist = self._extract_leaf_items({section: {name: w}}) if w else [] + hlist = self._extract_leaf_items({section: {name: h}}) if h else [] + + if self.state == "rendered": + hlist = [] + + have_leaf_index = {} + for hdict in hlist: + sig = self._vrrp_leaf_sig(hdict) + have_leaf_index[sig] = hdict + + want_leaf_index = {} + for wdict in wlist: + sig = self._vrrp_leaf_sig(wdict) + want_leaf_index[sig] = wdict + + if self.state in ["replaced", "deleted"]: + for sig, hdict in have_leaf_index.items(): + wdict = want_leaf_index.get(sig, {}) + if self.state == "deleted" and wdict: + wdict = {} + if self.state == "replaced" and wdict and wdict != hdict: + wdict = {} + elif not wdict: + hdict = {} + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + if self.state in ["merged", "replaced", "rendered", "overridden"]: + for sig, wdict in want_leaf_index.items(): + hdict = have_leaf_index.get(sig, {}) + self.compare( + parsers=vrrp_parsers, + want={"vrrp": wdict}, + have={"vrrp": hdict}, + ) + + def _vrrp_leaf_sig(self, item): + """Build a hashable signature for a VRRP leaf dict for O(1) indexing.""" + if not isinstance(item, dict) or not item: + return () + + container = next(iter(item)) + inner = item[container] + + sig = [container] + + if isinstance(inner, dict) and "name" in inner: + sig.append(("name", inner["name"])) + + if isinstance(inner, dict): + for k, v in inner.items(): + if k == "name": + continue + if not isinstance(v, dict): + sig.append(k) + break + sig.append(k) + for leaf in v: + sig.append(leaf) + break + break + + return tuple(sig) + + def _list_to_named_dict(self, data): + """Convert all named-object lists to name-keyed dicts in-place. + + Replaces the three separate _vrrp_groups_list_to_dict, + _vrrp_sync_groups_list_to_dict, and _virtual_servers_list_to_dict + methods with a single helper. Also normalises real_server lists + inside virtual servers. + """ + # VRRP groups and sync_groups + vrrp = data.get("vrrp", {}) + for key in ("groups", "sync_groups"): + items = vrrp.get(key) + if isinstance(items, list): + vrrp[key] = { + item["name"]: item + for item in items + if isinstance(item, dict) and item.get("name") + } + + # Virtual servers + vss = data.get("virtual_servers") + if isinstance(vss, list): + new_vss = {} + for vs in vss: + if not isinstance(vs, dict): + continue + name = vs.get("name") + if not name: + continue + rs = vs.get("real_server") + if isinstance(rs, list): + vs["real_server"] = { + item["address"]: item + for item in rs + if isinstance(item, dict) and item.get("address") + } + new_vss[name] = vs + data["virtual_servers"] = new_vss + elif isinstance(vss, dict): + for vs in vss.values(): + if not isinstance(vs, dict): + continue + rs = vs.get("real_server") + if isinstance(rs, list): + vs["real_server"] = { + item["address"]: item + for item in rs + if isinstance(item, dict) and item.get("address") + } + + return data + + def _extract_leaf_items(self, data, path=None, parent_name=None): + path = path or [] + results = [] + + if isinstance(data, dict): + current_name = data.get("name", parent_name) + + for k, v in data.items(): + if k == "name" or (k == "snmp" and v == "disabled"): + continue + results.extend(self._extract_leaf_items(v, path + [k], current_name)) + return results + + leaf_key = path[-1] + top_key = path[0] + + if top_key in ["groups", "sync_groups"]: + subkeys = path[2:] + else: + subkeys = path[1:] + + nested = {leaf_key: data} + + for p in reversed(subkeys[:-1]): + nested = {p: nested} + if parent_name: + out = {top_key: {"name": parent_name}} + out[top_key].update(nested) + else: + out = {top_key: nested} + + results.append(out) + return results + + def _normalize_lists(self, node): + """ + Recursively normalize all lists inside a dict or list. + All lists are sorted to ensure consistent ordering for comparison. + """ + if isinstance(node, dict): + for k, v in node.items(): + if isinstance(v, list): + if all(not isinstance(i, (dict, list)) for i in v): + node[k] = sorted(v) + else: + for item in v: + self._normalize_lists(item) + elif isinstance(v, dict): + self._normalize_lists(v) + elif isinstance(node, list): + for item in node: + self._normalize_lists(item) + + def _extract_named_leafs(self, data, parent_name=None, prefix_key=None): + results = [] + + if prefix_key == "real_server" and isinstance(data, dict): + for d, server_data in data.items(): + if not isinstance(server_data, dict): + continue + + address = server_data.get("address") + if not address: + continue + + for k, v in server_data.items(): + if k == "address": + continue + + results.append( + { + "name": parent_name, + "real_server": { + "address": address, + k: v, + }, + }, + ) + return results + + if isinstance(data, dict): + current_name = data.get("name", parent_name) + + for k, v in data.items(): + if k == "name": + continue + + results.extend( + self._extract_named_leafs(v, current_name, k), + ) + + return results + + return [ + { + "name": parent_name, + prefix_key: data, + }, + ] + + def _prune_stubs(self, w, h, path=""): + wc = {} + hc = self._remove_defaults(h) + + if not self._remove_defaults(w) and remove_empties(hc): + self.commands = ["delete high-availability"] + return {}, {}, path + + for k, wg in (self._remove_defaults(w) or {}).items(): + next_path = f"{path} {k}".strip() + stub = self._cli_path(next_path) + hg = remove_empties(hc).get(k) + + if hg is None: + continue + + if not isinstance(wg, (dict, list)): + self.commands.append(f"delete high-availability {stub}") + hc.pop(k, None) + wc.pop(k, None) + continue + + if not wg: + self.commands.append(f"delete high-availability {stub}") + hc.pop(k, None) + wc.pop(k, None) + continue + + if isinstance(wg, list) and isinstance(hg, dict): + for item in wg: + name = item.get("name") + if not name: + continue + + if name in hg: + self.commands.append( + f"delete high-availability {stub} {name}", + ) + + hg.pop(name, None) + + if hg: + hc[k] = hg + else: + hc.pop(k, None) + + if self._remove_defaults(wg): + wc[k] = wg + else: + wc.pop(k, None) + + continue + + if isinstance(wg, dict) and isinstance(hg, dict): + wi, hi, p = self._prune_stubs(wg, hg, next_path) + + if wi: + wc[k] = wi + + if hi: + hc[k] = hi + else: + hc.pop(k, None) + + return wc, hc, path + + def _remove_defaults(self, data): + """Strip None and False from config dicts, but preserve "disabled". + + False is the argspec default for boolean flags (disable, no_preempt, + rfc3768_compatibility) and carries no config intent — stripping it + prevents spurious `delete` commands for fields already at their + default state. + + "disabled" is an explicit user choice for snmp and must be preserved + so that _prune_stubs can act on it. The original code stripped it, + which made `snmp: disabled` invisible to the deleted-state logic. + """ + if isinstance(data, dict): + cleaned = {} + for k, v in data.items(): + if v is None or v is False: + continue + v = self._remove_defaults(v) + cleaned[k] = v + return cleaned + return data + + def _cli_path(self, path): + token_map = { + "groups": "group", + "sync_groups": "sync-group", + "virtual_servers": "virtual-server", + } + + parts = [] + for p in path.split(): + p = token_map.get(p, p) + parts.append(p.replace("_", "-")) + + return " ".join(parts) + + def _diff_w_h(self, w, h): + + NAMED_OBJECT_KEYS = { + "groups", + "sync_groups", + "virtual_servers", + "global_parameters", + } + + if not isinstance(w, dict) or not isinstance(h, dict): + return w + + for key in w.keys() & h.keys(): + wv = w[key] + hv = h[key] + + if key in NAMED_OBJECT_KEYS and isinstance(wv, dict) and isinstance(hv, dict): + for name in wv.keys() & hv.keys(): + if wv[name] != hv[name] and isinstance(wv[name], (dict, list)): + wv[name] = {} + elif wv[name] != hv[name]: + wv[name] = None + continue + self._diff_w_h(wv, hv) + return w diff --git a/plugins/module_utils/network/vyos/config/hostname/hostname.py b/plugins/module_utils/network/vyos/config/hostname/hostname.py index 36aba74f..8b30a693 100644 --- a/plugins/module_utils/network/vyos/config/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/config/hostname/hostname.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py index a9d9307c..71e4c0d1 100644 --- a/plugins/module_utils/network/vyos/config/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/config/interfaces/interfaces.py @@ -11,12 +11,10 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -76,6 +74,7 @@ class Interfaces(ConfigBase): if self.state in self.ACTION_STATES: existing_interfaces_facts = self.get_interfaces_facts() + else: existing_interfaces_facts = [] @@ -227,7 +226,7 @@ class Interfaces(ConfigBase): updates = dict_diff(have_copy, want_copy) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): commands.append( self._compute_commands(key=key, value=value, interface=want_copy["name"]), ) @@ -243,7 +242,7 @@ class Interfaces(ConfigBase): vif_updates = dict_diff(have_vif, want_vif) if vif_updates: - for key, value in iteritems(vif_updates): + for key, value in vif_updates.items(): commands.append( self._compute_commands( key=key, diff --git a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py index 6e0c005f..cfce6fee 100644 --- a/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/config/l3_interfaces/l3_interfaces.py @@ -13,13 +13,11 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -80,7 +78,7 @@ class L3_interfaces(ConfigBase): commands = list() if self.state in self.ACTION_STATES: - existing_l3_interfaces_facts = self.get_l3_interfaces_facts() + existing_l3_interfaces_facts = self.mutate_autoconfig(self.get_l3_interfaces_facts()) else: existing_l3_interfaces_facts = [] @@ -96,16 +94,19 @@ class L3_interfaces(ConfigBase): result["commands"] = commands if self.state in self.ACTION_STATES or self.state == "gathered": - changed_l3_interfaces_facts = self.get_l3_interfaces_facts() + changed_l3_interfaces_facts = self.mutate_autoconfig(self.get_l3_interfaces_facts()) elif self.state == "rendered": result["rendered"] = commands elif self.state == "parsed": running_config = self._module.params["running_config"] + if not running_config: self._module.fail_json( msg="value of running_config parameter must not be empty for state parsed", ) - result["parsed"] = self.get_l3_interfaces_facts(data=running_config) + result["parsed"] = self.mutate_autoconfig( + self.get_l3_interfaces_facts(data=running_config), + ) else: changed_l3_interfaces_facts = [] @@ -129,6 +130,7 @@ class L3_interfaces(ConfigBase): """ want = self._module.params["config"] have = existing_l3_interfaces_facts + resp = self.set_state(want, have) return to_list(resp) @@ -174,6 +176,7 @@ class L3_interfaces(ConfigBase): elif state == "replaced": commands.extend(self._state_replaced(item, obj_in_have)) + commands = [command.replace("auto-config", "autoconf") for command in commands] return commands def _state_replaced(self, want, have): @@ -226,7 +229,7 @@ class L3_interfaces(ConfigBase): have_vifs = have_copy.pop("vifs", []) for update in self._get_updates(want_copy, have_copy): - for key, value in iteritems(update): + for key, value in update.items(): commands.append( self._compute_commands(key=key, value=value, interface=want_copy["name"]), ) @@ -238,7 +241,7 @@ class L3_interfaces(ConfigBase): have_vif = {} for update in self._get_updates(want_vif, have_vif): - for key, value in iteritems(update): + for key, value in update.items(): commands.append( self._compute_commands( key=key, @@ -247,12 +250,15 @@ class L3_interfaces(ConfigBase): vif=want_vif["vlan_id"], ), ) - return commands def _state_deleted(self, want, have): """The command generator when state is deleted + Deletes only the L3 address attributes (base interface and VIFs) + owned by this module, never the interface subtree, so L2 settings + are preserved. + :rtype: A list :returns: the commands necessary to remove the current configuration of the provided objects @@ -261,51 +267,92 @@ class L3_interfaces(ConfigBase): want_copy = deepcopy(remove_empties(want)) have_copy = deepcopy(have) - want_vifs = want_copy.pop("vifs", []) - have_vifs = have_copy.pop("vifs", []) - - for update in self._get_updates(have_copy, want_copy): - for key, value in iteritems(update): - commands.append( - self._compute_commands( - key=key, - value=value, - interface=want_copy["name"], - remove=True, - ), - ) - - if have_vifs: - for have_vif in have_vifs: - want_vif = search_obj_in_list(have_vif["vlan_id"], want_vifs, key="vlan_id") - if not want_vif: - want_vif = {"vlan_id": have_vif["vlan_id"]} + if have_copy is not None: + if all(v in (None, {}, []) for k, v in want_copy.items() if k != "name"): + # Only delete L3 attributes we own — do not touch L2 config + have_vifs = have_copy.pop("vifs", []) or [] - for update in self._get_updates(have_vif, want_vif): - for key, value in iteritems(update): + for addr_family in ("ipv4", "ipv6"): + for addr in have_copy.get(addr_family) or []: commands.append( self._compute_commands( - key=key, + key="address", + value=addr["address"], interface=want_copy["name"], - value=value, - vif=want_vif["vlan_id"], remove=True, ), ) + for have_vif in have_vifs: + for addr_family in ("ipv4", "ipv6"): + for addr in have_vif.get(addr_family) or []: + commands.append( + self._compute_commands( + key="address", + value=addr["address"], + interface=want_copy["name"], + vif=have_vif["vlan_id"], + remove=True, + ), + ) + + return commands + + want_vifs = want_copy.pop("vifs", []) + have_vifs = have_copy.pop("vifs", []) + + if have_vifs: + for have_vif in have_vifs: + want_vif = search_obj_in_list(have_vif["vlan_id"], want_vifs, key="vlan_id") + if not want_vif: + want_vif = {"vlan_id": have_vif["vlan_id"]} + + for update in self._get_updates(have_vif, want_vif): + for key, value in update.items(): + commands.append( + self._compute_commands( + key=key, + interface=want_copy["name"], + value=value, + vif=want_vif["vlan_id"], + remove=True, + ), + ) + + for update in self._get_updates(have_copy, want_copy): + for key, value in update.items(): + commands.append( + self._compute_commands( + key=key, + value=value, + interface=want_copy["name"], + remove=True, + ), + ) + return commands def _compute_commands(self, interface, key, vif=None, value=None, remove=False): - intf_context = "interfaces {0} {1}".format(get_interface_type(interface), interface) + if value == "auto-config" and vif is None: + intf_context = "interfaces {0} {1} ipv6".format( + get_interface_type(interface), + interface, + ) + else: + intf_context = "interfaces {0} {1}".format(get_interface_type(interface), interface) + set_cmd = "set {0}".format(intf_context) del_cmd = "delete {0}".format(intf_context) if vif: - set_cmd = set_cmd + (" vif {0}".format(vif)) - del_cmd = del_cmd + (" vif {0}".format(vif)) + suffix = " ipv6" if value == "auto-config" else "" + set_cmd += f" vif {vif}{suffix}" + del_cmd += f" vif {vif}{suffix}" - if remove: + if remove and key and value: command = "{0} {1} '{2}'".format(del_cmd, key, value) + elif remove and not (key and value): + command = "{0}".format(del_cmd) else: command = "{0} {1} '{2}'".format(set_cmd, key, value) @@ -318,3 +365,12 @@ class L3_interfaces(ConfigBase): updates.extend(diff_list_of_dicts(want.get("ipv6", []), have.get("ipv6", []))) return updates + + def mutate_autoconfig(self, obj): + if isinstance(obj, dict): + return dict(map(lambda kv: (kv[0], self.mutate_autoconfig(kv[1])), obj.items())) + if isinstance(obj, list): + return list(map(self.mutate_autoconfig, obj)) + if isinstance(obj, str): + return obj.replace("autoconf", "auto-config") + return obj diff --git a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py index 6890fe0c..203519e4 100644 --- a/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/config/lag_interfaces/lag_interfaces.py @@ -8,11 +8,11 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.six import iteritems + from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -245,7 +245,7 @@ class Lag_interfaces(ConfigBase): commands.extend(self._add_bond_members(want, have)) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value: if key == "arp_monitor": commands.extend(self._add_arp_monitor(updates, key, want, have)) diff --git a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py index 1dfd25e4..82a35a54 100644 --- a/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/config/lldp_global/lldp_global.py @@ -8,11 +8,11 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type -from ansible.module_utils.six import iteritems + from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -241,7 +241,7 @@ class Lldp_global(ConfigBase): commands.extend(self._add_management_addresses(want, have)) if updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value is not None: if key == "enable": if value is False: diff --git a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py index 2fd6a548..36bf1b2d 100644 --- a/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/config/lldp_interfaces/lldp_interfaces.py @@ -13,10 +13,8 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -285,7 +283,7 @@ class Lldp_interfaces(ConfigBase): have_dict = have_location_type.get("coordinate_based") or {} location_type = "coordinate-based" updates = dict_diff(have_dict, want_dict) - for key, value in iteritems(updates): + for key, value in updates.items(): if value: commands.append(self._compute_command(set_cmd + location_type, key, str(value))) @@ -319,7 +317,7 @@ class Lldp_interfaces(ConfigBase): if is_dict_element_present(have_location_type, "coordinate_based"): have_dict = have_location_type.get("coordinate_based") or {} location_type = "coordinate-based" - for key, value in iteritems(have_dict): + for key, value in have_dict.items(): only_in_have = key_value_in_dict(key, value, want_dict) if not only_in_have: commands.append( diff --git a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py index f94c9195..1724f338 100644 --- a/plugins/module_utils/network/vyos/config/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/config/logging_global/logging_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -33,6 +31,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import ( Logging_globalTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import ( + Logging_globalTemplate15, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Logging_global(ResourceModule): @@ -48,22 +53,25 @@ class Logging_global(ResourceModule): resource="logging_global", tmplt=Logging_globalTemplate(), ) - self.parsers = [ - "console.facilities", - "global_params.archive.file_num", - "global_params.archive.size", - "global_params.marker_interval", - "global_params.preserve_fqdn", - "global_params.facilities", - "files.archive.size", - "files.archive.file_num", - "files", - "hosts.port", - "hosts.facility.protocol", # 1.3 and below - "hosts.protocol", - "hosts", - "users", - ] + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.5"): + self._tmplt = Logging_globalTemplate15() + else: + self._tmplt = Logging_globalTemplate() + + self.parsers = [p["name"] for p in self._tmplt.PARSERS if not p["name"].endswith(".state")] + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) def execute_module(self): """Execute the module @@ -71,11 +79,31 @@ class Logging_global(ResourceModule): :rtype: A dictionary :returns: The result from module execution """ + self._validate_template() if self.state not in ["parsed", "gathered"]: self.generate_commands() self.run_commands() return self.result + def _strip_unsupported_15(self, data): + """Remove 1.4-only keys from a list_to_dict result for 1.5 devices.""" + if not data: + return data + warnings = [] + for key in ("files", "users"): + if data.pop(key, None) is not None: + warnings.append( + "'{0}' is not supported on VyOS 1.5+, ignoring.".format(key), + ) + if "global_params" in data: + if data["global_params"].pop("archive", None) is not None: + warnings.append( + "'global_params.archive' is not supported on VyOS 1.5+, ignoring.", + ) + for warning in warnings: + self._module.warn(warning) + return data + def generate_commands(self): """Generate configuration commands to send based on want, have and desired state. @@ -89,10 +117,15 @@ class Logging_global(ResourceModule): else: haved = dict() + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.5"): + wantd = self._strip_unsupported_15(wantd) + haved = self._strip_unsupported_15(haved) + if self.state in ["overridden", "replaced"]: if wantd != haved: wantx, havex = self.call_op(wantd, haved, "overridden") - for k, have in iteritems(havex): + for k, have in havex.items(): if k not in wantx: self._compare(want={}, have=have) @@ -102,7 +135,7 @@ class Logging_global(ResourceModule): if self.state == "merged": wantd = dict_merge(haved, wantd) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -116,12 +149,12 @@ class Logging_global(ResourceModule): def operation_rep(self, params): op_val = dict() - for k, val in iteritems(params): + for k, val in params.items(): if k in ["console", "global_params"]: mod_val = deepcopy(val) op_val.update(self.flatten_facility({k: mod_val})) elif k in ["files", "hosts", "users"]: - for m, n in iteritems(val): + for m, n in val.items(): mod_n = deepcopy(n) if mod_n.get("archive"): del mod_n["archive"] @@ -160,18 +193,18 @@ class Logging_global(ResourceModule): def flatten_facility(self, param): temp_param = dict() - for element, val in iteritems(param): + for element, val in param.items(): if element in ["console", "global_params", "syslog"]: if element != "syslog" and val.get("facilities"): - for k, v in iteritems(val.get("facilities")): + for k, v in val.get("facilities").items(): temp_param[k + element] = {element: {"facilities": v}} del val["facilities"] if val: temp_param[element] = {element: val} if element in ["files", "hosts", "users"]: - for k, v in iteritems(val): + for k, v in val.items(): if v.get("facilities"): - for pk, dat in iteritems(v.get("facilities")): + for pk, dat in v.get("facilities").items(): temp_param[pk + k] = { element: { "facilities": dat, @@ -197,7 +230,7 @@ class Logging_global(ResourceModule): "hosts": "hostname", "users": "username", } - for element, val in iteritems(param): + for element, val in param.items(): if element == "facilities": # only with recursion call _tem_par = {} for par in val: diff --git a/plugins/module_utils/network/vyos/config/nat/__init__.py b/plugins/module_utils/network/vyos/config/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/config/nat/nat.py b/plugins/module_utils/network/vyos/config/nat/nat.py new file mode 100644 index 00000000..a2800914 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/nat/nat.py @@ -0,0 +1,577 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import ( + NatTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine + + +class Nat(ResourceModule): + """The vyos_nat config class""" + + def __init__(self, module): + super(Nat, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="nat", + tmplt=NatTemplate(), + ) + self.parsers = [] + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + + if self.state == "merged": + wantd = combine(haved, wantd, recursive=True) + + if self.state == "deleted": + if not wantd: + for nat_type in haved: + self.commands.append(f"delete {nat_type}") + return + self._list_to_dict(wantd) + self._list_to_dict(haved) + self._delete_nat_objects(wantd, haved, only_missing=False) + return + + self._list_to_dict(wantd) + self._list_to_dict(haved) + + if self.state == "replaced": + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + elif self.state == "overridden": + self._delete_nat_objects(wantd, haved, only_missing=True) + self._delete_nat_objects(wantd, haved, only_missing=False) + self._set_commands(wantd, haved) + else: + self._set_commands(wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # List → keyed dict conversion + # ------------------------------------------------------------------------- + + def _list_to_dict(self, config): + nat = config.get("nat", {}) + cgnat = nat.get("cgnat", {}) + + pool = cgnat.get("pool", {}) + for ptype in ("external", "internal"): + entries = pool.get(ptype) + if isinstance(entries, list): + pool[ptype] = {item["name"]: item for item in entries} + + rules = cgnat.get("rule") + if isinstance(rules, list): + cgnat["rule"] = {r["id"]: r for r in rules} + + for section in ("destination", "source", "static"): + rules = nat.get(section, {}).get("rule") + if isinstance(rules, list): + nat[section]["rule"] = {r["id"]: r for r in rules} + + nat64 = config.get("nat64", {}) + rules = nat64.get("source", {}).get("rule") + if isinstance(rules, list): + nat64["source"]["rule"] = {r["id"]: r for r in rules} + for rule in nat64["source"]["rule"].values(): + pools = rule.get("translation", {}).get("pool") + if isinstance(pools, list): + rule["translation"]["pool"] = {p["id"]: p for p in pools} + + nat66 = config.get("nat66", {}) + for section in ("destination", "source"): + rules = nat66.get(section, {}).get("rule") + if isinstance(rules, list): + nat66[section]["rule"] = {r["id"]: r for r in rules} + + # ------------------------------------------------------------------------- + # Top-level dispatch + # ------------------------------------------------------------------------- + + def _set_commands(self, wantd, haved): + self._compare_cgnat_global(wantd, haved) + self._compare_cgnat_pools(wantd, haved) + self._compare_cgnat_rules(wantd, haved) + + for section in ("destination", "source", "static"): + self._compare_nat_rules("nat", section, wantd, haved) + + self._compare_nat_rules("nat64", "source", wantd, haved) + + for section in ("destination", "source"): + self._compare_nat_rules("nat66", section, wantd, haved) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------------- + # Delete helpers + # ------------------------------------------------------------------------- + + def _delete_nat_objects(self, wantd, haved, only_missing=False): + """ + Generate delete commands for NAT objects. + only_missing=False: delete objects present in both want and have (when different) + only_missing=True: delete objects present in have but absent from want + """ + for nat_type in haved: + want_nat = wantd.get(nat_type, {}) + have_nat = haved[nat_type] + + if only_missing and nat_type not in wantd: + self.commands.append(f"delete {nat_type}") + continue + + for section in have_nat: + want_section = want_nat.get(section, {}) + have_section = have_nat[section] + + if only_missing and section not in want_nat: + self.commands.append( + f"delete {nat_type} {section.replace('_', '-')}", + ) + continue + + if section == "cgnat": + for pool_type in ("external", "internal"): + want_pools = want_section.get("pool", {}).get(pool_type, {}) + have_pools = have_section.get("pool", {}).get(pool_type, {}) + for name in have_pools: + if only_missing and name not in want_pools: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + + elif not only_missing and name in want_pools: + if self.state == "deleted" or want_pools[name] != have_pools[name]: + self.commands.append( + f"delete {nat_type} cgnat pool {pool_type} {name}", + ) + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append(f"delete {nat_type} cgnat rule {rid}") + else: + want_rules = want_section.get("rule", {}) + have_rules = have_section.get("rule", {}) + cli_section = section.replace("_", "-") + for rid in have_rules: + if only_missing and rid not in want_rules: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + elif not only_missing and rid in want_rules: + if self.state == "deleted" or want_rules[rid] != have_rules[rid]: + self.commands.append( + f"delete {nat_type} {cli_section} rule {rid}", + ) + + # ------------------------------------------------------------------------- + # CGNAT + # ------------------------------------------------------------------------- + + def _compare_cgnat_global(self, wantd, haved): + if self.state in ("replaced", "overridden") and not wantd.get("nat", {}).get("cgnat"): + return + w = wantd.get("nat", {}).get("cgnat", {}).get("log_allocation") + h = haved.get("nat", {}).get("cgnat", {}).get("log_allocation") + if bool(w) != bool(h): + self.addcmd( + {"nat": {"cgnat": {"log_allocation": True}}}, + "cgnat_log_allocation", + not bool(w), + ) + + def _compare_cgnat_pools(self, wantd, haved): + want_ext = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + have_ext = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("external", {}) + want_int = wantd.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + have_int = haved.get("nat", {}).get("cgnat", {}).get("pool", {}).get("internal", {}) + + scope = self.state in ("replaced", "overridden") + ext_names = set(want_ext) if scope else set(want_ext) | set(have_ext) + int_names = set(want_int) if scope else set(want_int) | set(have_int) + + for name in ext_names: + w = want_ext.get(name, {}) + h = have_ext.get(name, {}) + if scope and w != h: + h = {} + self._compare_external_pool(name, w, h) + + for name in int_names: + w = want_int.get(name, {}) + h = have_int.get(name, {}) + if scope and w != h: + h = {} + self._compare_internal_pool(name, w, h) + + def _compare_external_pool(self, name, want, have): + w = want.get("external_port_range") + h = have.get("external_port_range") + if w != h: + if w: + self.addcmd({"name": name, "range": w}, "cgnat_pool_external_port_range", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "range": h}, "cgnat_pool_external_port_range", True) + + w = want.get("per_user_limit", {}).get("port") + h = have.get("per_user_limit", {}).get("port") + if w != h: + if w: + self.addcmd({"name": name, "limit": w}, "cgnat_pool_external_per_user", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"name": name, "limit": h}, "cgnat_pool_external_per_user", True) + + want_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in want.get("range", [])} + have_ranges = {(r["value"] if isinstance(r, dict) else r): r for r in have.get("range", [])} + for val, rng in want_ranges.items(): + existing = have_ranges.get(val) + if existing is None or existing != rng: + seq = rng.get("seq") if isinstance(rng, dict) else None + self.addcmd( + {"name": name, "range": val, "seq": seq}, + "cgnat_pool_external_range", + False, + ) + + if self.state in ("replaced", "overridden"): + for val in have_ranges: + if val not in want_ranges: + self.addcmd({"name": name, "range": val}, "cgnat_pool_external_range", True) + + def _compare_internal_pool(self, name, want, have): + want_ranges = set(want.get("range", [])) + have_ranges = set(have.get("range", [])) + + for rng in want_ranges - have_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", False) + + if self.state in ("replaced", "overridden"): + for rng in have_ranges - want_ranges: + self.addcmd({"name": name, "range": rng}, "cgnat_pool_internal_range", True) + + def _compare_cgnat_rules(self, wantd, haved): + want_rules = wantd.get("nat", {}).get("cgnat", {}).get("rule", {}) + have_rules = haved.get("nat", {}).get("cgnat", {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + + if self.state in ("replaced", "overridden") and w != h: + h = {} + + w_src = w.get("source", {}).get("pool") + h_src = h.get("source", {}).get("pool") + if w_src != h_src: + if w_src: + self.addcmd({"id": rid, "pool": w_src}, "cgnat_rule_source_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_src}, "cgnat_rule_source_pool", True) + + w_tr = w.get("translation", {}).get("pool") + h_tr = h.get("translation", {}).get("pool") + if w_tr != h_tr: + if w_tr: + self.addcmd({"id": rid, "pool": w_tr}, "cgnat_rule_translation_pool", False) + elif self.state in ("replaced", "overridden"): + self.addcmd({"id": rid, "pool": h_tr}, "cgnat_rule_translation_pool", True) + + # ------------------------------------------------------------------------- + # NAT / NAT64 / NAT66 rules + # ------------------------------------------------------------------------- + + def _compare_nat_rules(self, nat_type, section, wantd, haved): + want_rules = wantd.get(nat_type, {}).get(section, {}).get("rule", {}) + have_rules = haved.get(nat_type, {}).get(section, {}).get("rule", {}) + + rids = ( + set(want_rules) + if self.state in ("replaced", "overridden") + else set(want_rules) | set(have_rules) + ) + + for rid in rids: + w = want_rules.get(rid, {}) + h = have_rules.get(rid, {}) + if self.state in ("replaced", "overridden") and w != h: + h = {} + if w == h and self.state != "rendered": + continue + self._compare_rule(nat_type, section, rid, w, h) + + def _compare_rule(self, nat_type, section, rid, want, have): + ctx = {"nat": nat_type, "type": section, "id": rid} + + want_lb = want.get("load_balance") or {} + have_lb = have.get("load_balance") or {} + want_trans_addr = (want.get("translation") or {}).get("address") + have_trans_addr = (have.get("translation") or {}).get("address") + if want_lb and want_trans_addr is not None: + self._module.fail_json( + msg="translation.address and load_balance are mutually exclusive", + ) + if self.state == "merged": + if want_lb and have_trans_addr is not None: + self._module.fail_json( + msg=( + "Cannot add load_balance to a rule that already has translation.address with " + "state=merged; use state=replaced or state=overridden" + ), + ) + if want_trans_addr is not None and have_lb: + self._module.fail_json( + msg=( + "Cannot add translation.address to a rule that already has load_balance with " + "state=merged; use state=replaced or state=overridden" + ), + ) + + for field in set(want) | set(have): + if field == "inbound_interface": + continue + val = want.get(field) if field in want else have.get(field) + if isinstance(val, bool): + self._cmp_bool(want, have, field, ctx, f"nat_type_{field}") + elif isinstance(val, str): + self._cmp_scalar(want, have, field, ctx, f"nat_type_{field}") + + self._cmp_interface(want, have, ctx, nat_type, section) + self._cmp_outbound_interface(want, have, ctx) + for atype in ("destination", "source"): + self._cmp_addr_sub(want, have, atype, ctx) + self._cmp_translation(want, have, ctx) + self._cmp_match_mark(want, have, ctx) + self._cmp_nat64_pools(want, have, ctx) + self._cmp_load_balance(want, have, ctx) + + # ------------------------------------------------------------------------- + # Field-level helpers + # ------------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser): + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h: + if w: + self.addcmd(dict(ctx), parser, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx), parser, True) + + def _cmp_interface(self, want, have, ctx, nat_type, section): + iface_w = want.get("inbound_interface") + iface_h = have.get("inbound_interface") + if iface_w == iface_h: + return + + if nat_type == "nat" and section == "static": + if iface_w: + self.addcmd(dict(ctx, value=iface_w), "nat_static_inbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h), "nat_static_inbound_interface", True) + return + + iface_w = iface_w or {} + iface_h = iface_h or {} + + if nat_type == "nat": + parser_name = "nat_inbound_interface_name" + parser_group = "nat_inbound_interface_group" + else: + parser_name = "nat6x_inbound_interface" + parser_group = "nat6x_inbound_interface" + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), parser_name, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), parser_name, True) + + if nat_type == "nat" and iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd(dict(ctx, value=iface_w["group"]), parser_group, False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["group"]), parser_group, True) + + def _cmp_outbound_interface(self, want, have, ctx): + iface_w = want.get("outbound_interface") or {} + iface_h = have.get("outbound_interface") or {} + + if iface_w.get("name") != iface_h.get("name"): + if iface_w.get("name"): + self.addcmd(dict(ctx, value=iface_w["name"]), "nat_type_outbound_interface", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, value=iface_h["name"]), "nat_type_outbound_interface", True) + + if iface_w.get("group") != iface_h.get("group"): + if iface_w.get("group"): + self.addcmd( + dict(ctx, value=iface_w["group"]), + "nat_type_outbound_interface_group", + False, + ) + elif self.state in ("replaced", "overridden"): + self.addcmd( + dict(ctx, value=iface_h["group"]), + "nat_type_outbound_interface_group", + True, + ) + + def _cmp_addr_sub(self, want, have, atype, ctx): + sub_w = want.get(atype) or {} + sub_h = have.get(atype) or {} + if sub_w == sub_h: + return + + changed = {k: v for k, v in sub_w.items() if sub_h.get(k) != v} + removed = { + k: v + for k, v in sub_h.items() + if k not in sub_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, atype=atype, sub=changed), "nat_type_address", False) + if removed: + self.addcmd(dict(ctx, atype=atype, sub=removed), "nat_type_address", True) + + def _cmp_translation(self, want, have, ctx): + trans_w = want.get("translation") or {} + trans_h = have.get("translation") or {} + if trans_w == trans_h: + return + + changed = {k: v for k, v in trans_w.items() if k != "pool" and trans_h.get(k) != v} + removed = { + k: v + for k, v in trans_h.items() + if k != "pool" and k not in trans_w and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd(dict(ctx, translation=changed), "nat_type_translation_address", False) + if removed: + self.addcmd(dict(ctx, translation=removed), "nat_type_translation_address", True) + + def _cmp_match_mark(self, want, have, ctx): + w = want.get("match", {}).get("mark") + h = have.get("match", {}).get("mark") + if w != h: + if w is not None: + self.addcmd(dict(ctx, mark=w), "nat64_match_mark", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, mark=h), "nat64_match_mark", True) + + def _cmp_nat64_pools(self, want, have, ctx): + want_pools = want.get("translation", {}).get("pool", {}) + have_pools = have.get("translation", {}).get("pool", {}) + + if isinstance(want_pools, list): + want_pools = {p["id"]: p for p in want_pools} + if isinstance(have_pools, list): + have_pools = {p["id"]: p for p in have_pools} + + for pid in set(want_pools) | set(have_pools): + wp = want_pools.get(pid, {}) + hp = have_pools.get(pid, {}) + + if wp == hp: + continue + + changed = {k: v for k, v in wp.items() if k != "id" and hp.get(k) != v} + removed = { + k: v + for k, v in hp.items() + if k != "id" and k not in wp and self.state in ("replaced", "overridden") + } + + if changed: + self.addcmd( + dict(ctx, pool_id=pid, pool=changed), + "nat64_translation_pool", + False, + ) + if removed: + self.addcmd( + dict(ctx, pool_id=pid, pool=removed), + "nat64_translation_pool", + True, + ) + + def _cmp_load_balance(self, want, have, ctx): + lb_w = want.get("load_balance") or {} + lb_h = have.get("load_balance") or {} + + want_hash = set(lb_w.get("hash") or []) + have_hash = set(lb_h.get("hash") or []) + + for h in want_hash - have_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", False) + if self.state in ("replaced", "overridden"): + for h in have_hash - want_hash: + self.addcmd(dict(ctx, value=h), "nat_type_lb_hash", True) + + want_backends = lb_w.get("backend", []) + have_backends = lb_h.get("backend", []) + if isinstance(want_backends, list): + want_backends = {b["ip"]: b for b in want_backends} + if isinstance(have_backends, list): + have_backends = {b["ip"]: b for b in have_backends} + + for ip in set(want_backends) | set(have_backends): + wb = want_backends.get(ip, {}) + hb = have_backends.get(ip, {}) + if wb == hb: + continue + if wb: + weight = wb.get("weight") + if weight is None: + self._module.fail_json(msg="load_balance.backend entries require 'weight'") + self.addcmd(dict(ctx, ip=ip, weight=weight), "nat_type_lb_backend", False) + elif self.state in ("replaced", "overridden"): + self.addcmd(dict(ctx, ip=ip, weight=hb.get("weight")), "nat_type_lb_backend", True) diff --git a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py index 5d294063..78b7a545 100644 --- a/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/config/ntp_global/ntp_global.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -18,7 +17,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -93,16 +91,16 @@ class Ntp_global(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} wantd = {} commandlist = self._commandlist(haved) servernames = self._servernames(haved) # removing the servername and commandlist from the list after deleting it from haved # iterate through the top-level items to delete - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: - for hk, hval in iteritems(have): + for hk, hval in have.items(): if hk == "allow_clients" and hk in commandlist: self.commands.append( self._tmplt.render({"": hk}, "allow_clients_delete", True), @@ -130,7 +128,7 @@ class Ntp_global(ResourceModule): commandlist = self._commandlist(haved) servernames = self._servernames(haved) - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: if "server" not in have: self._compareoverride(want={}, have=have) @@ -139,7 +137,7 @@ class Ntp_global(ResourceModule): self._compareoverride(want={}, have=have) servernames.remove(have["server"]) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -155,7 +153,7 @@ class Ntp_global(ResourceModule): def _compareoverride(self, want, have): # do not delete configuration with options level - for i, val in iteritems(have): + for i, val in have.items(): if i == "options": pass else: @@ -163,12 +161,12 @@ class Ntp_global(ResourceModule): def _ntp_list_to_dict(self, entry): servers_dict = {} - for k, data in iteritems(entry): + for k, data in entry.items(): if k == "servers": for value in data: if "options" in value: result = self._serveroptions_list_to_dict(value) - for res, resvalue in iteritems(result): + for res, resvalue in result.items(): servers_dict.update({res: resvalue}) else: servers_dict.update({value["server"]: value}) @@ -179,7 +177,7 @@ class Ntp_global(ResourceModule): def _serveroptions_list_to_dict(self, entry): serveroptions_dict = {} - for Opk, Op in iteritems(entry): + for Opk, Op in entry.items(): if Opk == "options": for val in Op: dict = {} @@ -190,16 +188,16 @@ class Ntp_global(ResourceModule): def _commandlist(self, haved): commandlist = [] - for k, have in iteritems(haved): - for ck, cval in iteritems(have): + for k, have in haved.items(): + for ck, cval in have.items(): if ck != "options" and ck not in commandlist: commandlist.append(ck) return commandlist def _servernames(self, haved): servernames = [] - for k, have in iteritems(haved): - for sk, sval in iteritems(have): + for k, have in haved.items(): + for sk, sval in have.items(): if sk != "options" and sval not in servernames: servernames.append(sval) return servernames diff --git a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py index 51b47494..bc93ac24 100644 --- a/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/config/ospf_interfaces/ospf_interfaces.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ The vyos_ospf_interfaces config file. @@ -17,7 +16,6 @@ necessary to bring the current configuration to its desired end-state is created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -27,17 +25,16 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.u from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces import ( - Ospf_interfacesTemplate + Ospf_interfacesTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( - Ospf_interfacesTemplate14 + Ospf_interfacesTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Ospf_interfaces(ResourceModule): """ @@ -77,7 +74,7 @@ class Ospf_interfaces(ResourceModule): self._tmplt = Ospf_interfacesTemplate() def parse(self): - """ override parse to check template """ + """override parse to check template""" self._validate_template() return super().parse() @@ -119,12 +116,12 @@ class Ospf_interfaces(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": h_del = {} - for k, v in iteritems(haved): + for k, v in haved.items(): if k in wantd or not wantd: h_del.update({k: v}) haved = h_del have_int = [] - for k, have in iteritems(haved): + for k, have in haved.items(): if k in wantd: have_int.append(k) self._remove_ospf_int(have) @@ -132,7 +129,7 @@ class Ospf_interfaces(ResourceModule): if self.state == "overridden": have_int = [] - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: have_int.append(k) self._remove_ospf_int(have) @@ -142,17 +139,17 @@ class Ospf_interfaces(ResourceModule): # removing the interfaces from haved that are already negated for interface in have_int: haved.pop(interface) - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _remove_ospf_int(self, entry): int_name = entry.get("name", {}) int_addr = entry.get("address_family", {}) - for k, addr in iteritems(int_addr): + for k, addr in int_addr.items(): rem_entry = {"name": int_name, "address_family": {"afi": k}} self.addcmd(rem_entry, "ip_ospf", True) @@ -169,8 +166,8 @@ class Ospf_interfaces(ResourceModule): hdict = have.get("address_family", {}) wname = want.get("name") hname = have.get("name") - for name, entry in iteritems(wdict): - for key, param in iteritems(entry): + for name, entry in wdict.items(): + for key, param in entry.items(): w_addr = {"afi": name, key: param} h_addr = {} if hdict.get(name): @@ -178,8 +175,8 @@ class Ospf_interfaces(ResourceModule): w = {"name": wname, "address_family": w_addr} h = {"name": hname, "address_family": h_addr} self.compare(parsers=self.parsers, want=w, have=h) - for name, entry in iteritems(hdict): - for key, param in iteritems(entry): + for name, entry in hdict.items(): + for key, param in entry.items(): h_addr = {"afi": name, key: param} w_addr = {} w = {"name": wname, "address_family": w_addr} @@ -187,7 +184,7 @@ class Ospf_interfaces(ResourceModule): self.compare(parsers=self.parsers, want=w, have=h) def _ospf_int_list_to_dict(self, entry): - for name, family in iteritems(entry): + for name, family in entry.items(): if "address_family" in family: addr_dict = {} for entry in family.get("address_family", []): diff --git a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py index a9c1de1b..20821980 100644 --- a/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/config/ospfv2/ospfv2.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -33,6 +32,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils _is_w_same, list_diff_want_only, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Ospfv2(ConfigBase): @@ -54,7 +57,7 @@ class Ospfv2(ConfigBase): :returns: The current configuration as a dictionary """ - (facts, _warnings) = Facts(self._module).get_facts( + facts, _warnings = Facts(self._module).get_facts( self.gather_subset, self.gather_network_resources, data=data, @@ -124,7 +127,7 @@ class Ospfv2(ConfigBase): want = self._module.params["config"] have = existing_ospfv2_facts - resp = self.set_state(want, have) + resp = self.set_state(remove_empties(want), remove_empties(have)) return to_list(resp) def set_state(self, w, h): @@ -136,7 +139,6 @@ class Ospfv2(ConfigBase): :returns: the commands necessary to migrate the current configuration to the desired configuration """ - commands = [] if self.state in ("merged", "replaced", "overridden", "rendered") and not w: self._module.fail_json( @@ -220,7 +222,7 @@ class Ospfv2(ConfigBase): w = deepcopy(remove_empties(want)) leaf = ("default_metric", "log_adjacency_changes") if w: - for key, val in iteritems(w): + for key, val in w.items(): if opr and key in leaf and not _is_w_same(w, have, key): commands.append(self._form_attr_cmd(attr=key, val=_bool_to_str(val), opr=opr)) elif not opr and key in leaf and not _in_target(have, key): @@ -270,7 +272,6 @@ class Ospfv2(ConfigBase): :param opr: True/False. :return: generated list of commands. """ - commands = [] h = {} if have: @@ -289,7 +290,7 @@ class Ospfv2(ConfigBase): ), } leaf = leaf_dict[attr] - for item, value in iteritems(want[attr]): + for item, value in want[attr].items(): if opr and item in leaf and not _is_w_same(want[attr], h, item): if item == "enabled": item = "enable" @@ -320,7 +321,6 @@ class Ospfv2(ConfigBase): :param opr: True/False. :return: generated list of commands. """ - commands = [] h = [] if want: @@ -336,6 +336,22 @@ class Ospfv2(ConfigBase): command = cmd + attr.replace("_", "-") + " " if attr == "network": command += member["address"] + elif ( + attr == "passive_interface" + and member != "default" + and LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + ): + command = ( + command.replace("passive-interface", "interface") + member + " passive" + ) + elif attr == "passive_interface_exclude" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): + command = ( + command.replace("passive-interface-exclude", "interface") + + member + + " passive disable" + ) else: command += member commands.append(command) @@ -348,7 +364,23 @@ class Ospfv2(ConfigBase): cmd + attr.replace("_", "-") + " " + member["address"], ) elif member not in h: - commands.append(cmd + attr.replace("_", "-") + " " + member) + if ( + attr == "passive_interface" + and member != "default" + and LooseVersion(get_os_version(self._module)) + >= LooseVersion("1.4") + ): + commands.append(cmd + "interface" + " " + member + " passive") + elif attr == "passive_interface_exclude" and LooseVersion( + get_os_version(self._module), + ) >= LooseVersion("1.4"): + command = ( + command.replace("passive-interface-exclude", "interface") + + member + + " passive disable" + ) + else: + commands.append(cmd + attr.replace("_", "-") + " " + member) else: commands.append(cmd + " " + attr.replace("_", "-")) return commands @@ -385,7 +417,7 @@ class Ospfv2(ConfigBase): commands.append(cmd + attr.replace("_", "-")) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = self.search_obj_in_have(h, w_item, name[attr]) @@ -493,7 +525,7 @@ class Ospfv2(ConfigBase): commands.append(self._compute_command(attr=attr, opr=opr)) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = self.search_obj_in_have(h, w_item, name[attr]) @@ -590,7 +622,7 @@ class Ospfv2(ConfigBase): leaf = leaf_dict[attr] if h and key in h.keys(): h_attrib = h.get(key) or {} - for item, val in iteritems(w[key]): + for item, val in w[key].items(): if opr and item in leaf and not _is_w_same(w[key], h_attrib, item): if item in ("administrative", "always") and val: commands.append( @@ -651,7 +683,7 @@ class Ospfv2(ConfigBase): self._form_attr_cmd(key="area", attr=w_area["area_id"], opr=opr), ) else: - for key, val in iteritems(w_area): + for key, val in w_area.items(): if opr and key in l_set and not _is_w_same(w_area, h_area, key): if key == "area_id": commands.append( @@ -724,7 +756,7 @@ class Ospfv2(ConfigBase): if w_area: if h_type and key in h_type.keys(): h_area = h_type.get(key) or {} - for item, val in iteritems(w_type[key]): + for item, val in w_type[key].items(): if ( opr and item in a_type[key] diff --git a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py index 25d9a0ea..a84899f5 100644 --- a/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/config/ospfv3/ospfv3.py @@ -10,14 +10,13 @@ is compared to the provided configuration (as dict) and the command set necessary to bring the current configuration to it's desired end-state is created """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -33,6 +32,10 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils _in_target, _is_w_same, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Ospfv3(ConfigBase): @@ -200,7 +203,7 @@ class Ospfv3(ConfigBase): commands = [] w = deepcopy(remove_empties(want)) if w: - for key, val in iteritems(w): + for key, val in w.items(): commands.extend(self._render_child_param(w, have, key, opr)) return commands @@ -241,7 +244,7 @@ class Ospfv3(ConfigBase): elif want[attr]: leaf_dict = {"parameters": "router_id"} leaf = leaf_dict[attr] - for item, value in iteritems(want[attr]): + for item, value in want[attr].items(): if opr and item in leaf and not _is_w_same(want[attr], h, item): commands.append(self._form_attr_cmd(key=attr, attr=item, val=value, opr=opr)) elif not opr and item in leaf and not _in_target(h, item): @@ -264,10 +267,12 @@ class Ospfv3(ConfigBase): name = { "redistribute": "route_type", "range": "address", + "interface": "name", } leaf_dict = { "redistribute": ("route_map", "route_type"), "range": ("address", "advertise", "not_advertise"), + "interface": ("name"), } leaf = leaf_dict[attr] w = want.get(attr) or [] @@ -277,19 +282,31 @@ class Ospfv3(ConfigBase): commands.append(self._compute_command(attr=attr, opr=opr)) elif w: for w_item in w: - for key, val in iteritems(w_item): + for key, val in w_item.items(): if not cmd: cmd = self._compute_command(opr=opr) h_item = search_obj_in_list(w_item[name[attr]], h, name[attr]) if opr and key in leaf and not _is_w_same(w_item, h_item, key): - if key == "route_type" or ( + if key in ["route_type", "name"] or ( key == "address" and "advertise" not in w_item and "not-advertise" not in w_item ): if not val: cmd = cmd.replace("set", "delete") - commands.append(cmd + attr + " " + str(val)) + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attr == "interface" + ): + words = cmd.split() + cmd14_list = [] + for word in words: + cmd14_list.append(word) + if word == "ospfv3": + cmd14_list.append(attr + " " + str(val)) + commands.append(" ".join(cmd14_list)) + else: + commands.append(cmd + attr + " " + str(val)) elif key in leaf_dict["range"] and key != "address": commands.append( cmd + attr + " " + w_item[name[attr]] + " " + key.replace("_", "-"), @@ -306,8 +323,20 @@ class Ospfv3(ConfigBase): + str(val), ) elif not opr and key in leaf and not _in_target(h_item, key): - if key in ("route_type", "address"): - commands.append(cmd + attr + " " + str(val)) + if key in ("route_type", "address", "name"): + if ( + LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4") + and attr == "interface" + ): + words = cmd.split() + cmd14_list = [] + for word in words: + cmd14_list.append(word) + if word == "ospfv3": + cmd14_list.append(attr + " " + str(val)) + commands.append(" ".join(cmd14_list)) + else: + commands.append(cmd + attr + " " + str(val)) else: commands.append(cmd + (attr + " " + w_item[name[attr]] + " " + key)) return commands @@ -346,7 +375,7 @@ class Ospfv3(ConfigBase): self._form_attr_cmd(key="area", attr=w_area["area_id"], opr=opr), ) else: - for key, val in iteritems(w_area): + for key, val in w_area.items(): if opr and key in l_set and not _is_w_same(w_area, h_area, key): if key == "area_id": commands.append( @@ -373,6 +402,10 @@ class Ospfv3(ConfigBase): commands.extend( self._render_list_dict_param(key, w_area, h_area, cmd, opr), ) + elif key == "interface": + commands.extend( + self._render_list_dict_param(key, w_area, h_area, cmd, opr), + ) return commands def _form_attr_cmd(self, key=None, attr=None, val=None, opr=True): diff --git a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py index 9da27c15..05164c31 100644 --- a/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/config/prefix_lists/prefix_lists.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -19,7 +18,6 @@ created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -86,23 +84,23 @@ class Prefix_lists(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} - for key, hvalue in iteritems(haved): + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} + for key, hvalue in haved.items(): wvalue = wantd.pop(key, {}) if wvalue: wplists = wvalue.get("prefix_lists", {}) hplists = hvalue.get("prefix_lists", {}) hvalue["prefix_lists"] = { - k: v for k, v in iteritems(hplists) if k in wplists or not wplists + k: v for k, v in hplists.items() if k in wplists or not wplists } # remove superfluous config for overridden and deleted if self.state in ["overridden", "deleted"]: - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self._compare(want={}, have=have) - for k, want in iteritems(wantd): + for k, want in wantd.items(): self._compare(want=want, have=haved.pop(k, {})) def _compare(self, want, have): @@ -127,7 +125,7 @@ class Prefix_lists(ResourceModule): ) def _compare_plists(self, want, have): - for wk, wentry in iteritems(want): + for wk, wentry in want.items(): hentry = have.pop(wk, {}) # parser list for name and descriptions @@ -143,7 +141,7 @@ class Prefix_lists(ResourceModule): self._compare_rules(want=wplrules, have=hplrules) def _compare_rules(self, want, have): - for wr, wrule in iteritems(want): + for wr, wrule in want.items(): hrule = have.pop(wr, {}) # parser list for entries @@ -164,7 +162,7 @@ class Prefix_lists(ResourceModule): ) def _prefix_list_list_to_dict(self, entry): - for afi, value in iteritems(entry): + for afi, value in entry.items(): if "prefix_lists" in value: for pl in value["prefix_lists"]: pl.update({"afi": afi}) diff --git a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py index 9b6c3e9d..948341d3 100644 --- a/plugins/module_utils/network/vyos/config/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/config/route_maps/route_maps.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -19,7 +18,6 @@ created. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -31,6 +29,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps import ( Route_mapsTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Route_maps(ResourceModule): @@ -59,6 +64,8 @@ class Route_maps(ResourceModule): "set_bgp_extcommunity_rt", "set_extcommunity_rt", "set_extcommunity_soo", + "set_extcommunity_bandwidth", + "set_extcommunity_bandwidth_non_transitive", "set_ip_next_hop", "set_ipv6_next_hop", "set_large_community", @@ -70,6 +77,7 @@ class Route_maps(ResourceModule): "set_src", "set_tag", "set_weight", + "set_table", "set_comm_list", "set_comm_list_delete", "set_community", @@ -89,15 +97,34 @@ class Route_maps(ResourceModule): "on_match_next", "match_ipv6_address", "match_ipv6_nexthop", + "match_protocol", "match_rpki", ] + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = Route_mapsTemplate14() + else: + self._tmplt = Route_mapsTemplate() + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) + def execute_module(self): """Execute the module :rtype: A dictionary :returns: The result from module execution """ + self._validate_template() if self.state not in ["parsed", "gathered"]: self.generate_commands() self.run_commands() @@ -116,16 +143,16 @@ class Route_maps(ResourceModule): # if state is deleted, empty out wantd and set haved to wantd if self.state == "deleted": - haved = {k: v for k, v in iteritems(haved) if k in wantd or not wantd} + haved = {k: v for k, v in haved.items() if k in wantd or not wantd} wantd = {} # remove superfluous config for overridden and deleted if self.state in ["overridden", "deleted"]: - for k, have in iteritems(haved): + for k, have in haved.items(): if k not in wantd: self.commands.append(self._tmplt.render({"route_map": k}, "route_map", True)) - for wk, want in iteritems(wantd): + for wk, want in wantd.items(): self._compare(want=want, have=haved.pop(wk, {})) def _compare(self, want, have): @@ -139,13 +166,13 @@ class Route_maps(ResourceModule): self._compare_entries(want=w_entries, have=h_entries) def _compare_entries(self, want, have): - for wk, wentry in iteritems(want): + for wk, wentry in want.items(): hentry = have.pop(wk, {}) self.compare(parsers=self.parsers, want=wentry, have=hentry) def _route_maps_list_to_dict(self, entry): entry = {x["route_map"]: x for x in entry} - for rmap, data in iteritems(entry): + for rmap, data in entry.items(): if "entries" in data: for x in data["entries"]: x.update({"route_map": rmap}) diff --git a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py index 9497d7fa..cdc1d6e1 100644 --- a/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/config/snmp_server/snmp_server.py @@ -7,7 +7,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -20,7 +19,6 @@ created. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( ResourceModule, ) @@ -107,13 +105,13 @@ class Snmp_server(ResourceModule): """ self._compare_lists(want, have) self._compare_snmp_v3(want, have) - for key, value in iteritems(want): + for key, value in want.items(): self.compare( parsers=self.parsers, want={key: value}, have={key: have.pop(key, "")}, ) - for key, entry in iteritems(have): + for key, entry in have.items(): if entry: self.compare(parsers=self.parsers, want={}, have={key: entry}) @@ -125,10 +123,10 @@ class Snmp_server(ResourceModule): for attrib in parsers: wdict = get_from_dict(want, attrib) or {} hdict = get_from_dict(have, attrib) or {} - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): # self.addcmd(entry, attrib, False) if attrib == "communities": - for k, v in iteritems(entry): + for k, v in entry.items(): if k in ["clients", "networks"]: v.sort() h = {} @@ -153,9 +151,9 @@ class Snmp_server(ResourceModule): ) have.pop(attrib, {}) # remove remaining items in have for replaced - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): if attrib == "communities": - for k, v in iteritems(entry): + for k, v in entry.items(): if k != "name": self.compare( parsers="communities", @@ -188,9 +186,9 @@ class Snmp_server(ResourceModule): for attrib in attribute_dict.keys(): wattrib = get_from_dict(wdict, attrib) or {} hattrib = get_from_dict(hdict, attrib) or {} - for key, entry in iteritems(wattrib): + for key, entry in wattrib.items(): self._compare_snmp_v3_auth_privacy(entry, hattrib.get(key, {}), attrib) - for k, v in iteritems(entry): + for k, v in entry.items(): if k != attribute_dict[attrib]: h = {} if hattrib.get(key): @@ -217,11 +215,11 @@ class Snmp_server(ResourceModule): have=h, ) # remove remaining items in have for replaced - for key, entry in iteritems(hattrib): + for key, entry in hattrib.items(): self._compare_snmp_v3_auth_privacy({}, entry, attrib) self.compare(parsers=parsers, want={}, have={"snmp_v3": {attrib: entry}}) hdict.pop(attrib, {}) - for key, entry in iteritems(wdict): + for key, entry in wdict.items(): # self.addcmd(entry, attrib, False) self.compare( parsers="snmp_v3.engine_id", @@ -229,7 +227,7 @@ class Snmp_server(ResourceModule): have={"snmp_v3": {key: hdict.pop(key, {})}}, ) # remove remaining items in have for replaced - for key, entry in iteritems(hdict): + for key, entry in hdict.items(): self.compare(parsers=parsers, want={}, have={"snmp_v3": {key: entry}}) def _compare_snmp_v3_auth_privacy(self, wattrib, hattrib, attrib): @@ -244,7 +242,7 @@ class Snmp_server(ResourceModule): primary_key = "user" else: primary_key = "address" - for key, entry in iteritems(wattrib): + for key, entry in wattrib.items(): if key != primary_key and key in ["authentication", "privacy"]: self.compare( parsers=parsers, @@ -265,7 +263,7 @@ class Snmp_server(ResourceModule): }, }, ) - for key, entry in iteritems(hattrib): + for key, entry in hattrib.items(): if key != primary_key and key in ["authentication", "privacy"]: self.compare( parsers=parsers, @@ -291,13 +289,13 @@ class Snmp_server(ResourceModule): "views": "view", "trap_targets": "address", } - for k, v in iteritems(param_dict): + for k, v in param_dict.items(): if k in entry: a_dict = {} for el in entry[k]: a_dict.update({el[v]: el}) entry[k] = a_dict - for k, v in iteritems(v3_param_dict): + for k, v in v3_param_dict.items(): if entry.get("snmp_v3") and k in entry.get("snmp_v3"): a_dict = {} for el in entry["snmp_v3"][k]: diff --git a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py index 8451e7da..2a09c0f0 100644 --- a/plugins/module_utils/network/vyos/config/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/config/static_routes/static_routes.py @@ -13,11 +13,9 @@ created from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( ConfigBase, ) @@ -182,7 +180,7 @@ class Static_routes(ConfigBase): """ commands = [] if have: - for key, value in iteritems(want): + for key, value in want.items(): if value: if key == "next_hops": commands.extend(self._update_next_hop(want, have)) @@ -257,7 +255,7 @@ class Static_routes(ConfigBase): """ commands = [] have = {} - for key, value in iteritems(want): + for key, value in want.items(): if value: if key == "dest": commands.append(self._compute_command(dest=want["dest"])) @@ -287,7 +285,7 @@ class Static_routes(ConfigBase): updates = dict_delete(want_blackhole, have_blackhole) if updates: - for attrib, value in iteritems(updates): + for attrib, value in updates.items(): if value: if attrib == "distance": commands.append( @@ -394,7 +392,7 @@ class Static_routes(ConfigBase): have_blackhole = have_copy.get(key) or {} updates = dict_delete(have_blackhole, want_blackhole) if updates: - for attrib, value in iteritems(updates): + for attrib, value in updates.items(): if value: if attrib == "distance": commands.append( @@ -508,7 +506,7 @@ class Static_routes(ConfigBase): commands.extend(self._add_next_hop(want, have, opr=opr)) if opr and updates: - for key, value in iteritems(updates): + for key, value in updates.items(): if value: if key == "blackhole_config": commands.extend(self._add_blackhole(key, want, have)) diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..2d986cb4 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,665 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos_vpn_ipsec config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. + +Follows the established per-module convention used by vyos_ha/vyos_nat +(list-to-dict conversion + explicit per-state branching in +generate_commands), rather than a shared generic engine. + +State semantics (standard Ansible RM convention, confirmed against a +real device run that caught a bug in an earlier version of this file): + - merged: only items/fields named in `want` are touched. Nothing + absent from `want` is ever deleted. + - replaced: only items NAMED in `want` are touched (same item scope + as merged) -- but for each named item, its full state is + reconciled to exactly match `want` (fields present in + `have` but omitted from `want` ARE deleted). Items not + named in `want` at all are left completely alone. + - overridden: every item is in scope, including ones absent from + `want` entirely -- those get deleted wholesale. Named + items are reconciled the same way as `replaced`. + +This is implemented via two independent flags: + - select_all: whether item iteration considers have-only items too + (True only for overridden; False for merged/replaced). + - reconcile: whether omitted fields within an already-selected item + get deleted (True for replaced/overridden; False for + merged/rendered). +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( + Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( + Vpn_ipsecTemplate, +) + + +class Vpn_ipsec(ResourceModule): + """ + The vyos_vpn_ipsec config class + """ + + def __init__(self, module): + super(Vpn_ipsec, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vpn_ipsec", + tmplt=Vpn_ipsecTemplate(), + ) + self.parsers = [ + "esp_group", + "esp_group.proposal", + "esp_group.proposal.encryption", + "esp_group.proposal.hash", + "esp_group.compression", + "esp_group.disable_rekey", + "esp_group.life_bytes", + "esp_group.life_packets", + "esp_group.lifetime", + "esp_group.mode", + "esp_group.pfs", + "ike_group", + "ike_group.key_exchange", + "ike_group.proposal", + "ike_group.proposal.dh_group", + "ike_group.proposal.encryption", + "ike_group.proposal.hash", + "ike_group.close_action", + "ike_group.dead_peer_detection.action", + "ike_group.dead_peer_detection.interval", + "ike_group.dead_peer_detection.timeout", + "ike_group.disable_mobike", + "ike_group.ikev2_reauth", + "ike_group.lifetime", + "ike_group.mode", + "profile", + "profile.authentication.mode", + "profile.authentication.pre_shared_secret", + "profile.esp_group", + "profile.ike_group", + "profile.disable", + "authentication.psk.secret_type", + "authentication.psk.dhcp_interface", + "authentication.ppk", + "authentication.ppk.id", + "authentication.ppk.secret", + "authentication.ppk.secret_type", + "interface", + "log.level", + "log.subsystem", + "options.disable_route_autoinstall", + "options.flexvpn", + "options.interface", + "options.retransmission.attempts", + "options.retransmission.base", + "options.retransmission.timeout", + "options.virtual_ip", + "disable_uniqreqids", + ] + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = deepcopy(self.want) or {} + haved = deepcopy(self.have) or {} + + for entry in (wantd, haved): + self._list_to_dict(entry) + + scoped_delete = None + if self.state == "deleted": + if wantd: + # user named specific items -- surgical removal of just + # those, everything else preserved (vyos_vrf precedent: + # deleted + instances:[{name: vrf-blue}] removes only + # vrf-blue). Capture what was named before wiping wantd. + scoped_delete = wantd + wantd = {} + + if self.state == "merged": + # NOTE: list_to_dict() above must run BEFORE this. dict_merge + # concatenates lists rather than merging matching entries by + # key, so merging while ike_group/esp_group/etc are still + # lists would duplicate entries instead of filling in omitted + # fields from `have`. Once they're name-keyed dicts, dict_merge + # recurses per-key correctly, which is what lets a partial + # update (e.g. specifying only key_exchange) leave other + # existing fields on that same group untouched. + wantd = dict_merge(haved, wantd) + + select_all = self.state in ("overridden", "deleted") + reconcile = self.state in ("replaced", "overridden", "deleted") + + self._compare_esp_groups(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_ike_groups(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_profiles(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_psks(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_ppks(wantd, haved, select_all, reconcile, scoped_delete) + self._compare_top_level(wantd, haved, select_all, reconcile, scoped_delete) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------- + # List -> name-keyed dict conversion (matches vyos_ha/vyos_nat style) + # ------------------------------------------------------------------- + + def _list_to_dict(self, config): + for key in ("ike_group", "esp_group", "profile"): + items = config.get(key) + if isinstance(items, list): + config[key] = {item["name"]: item for item in items} + for item in config[key].values(): + if isinstance(item.get("proposal"), list): + item["proposal"] = {p["proposal_id"]: p for p in item["proposal"]} + + auth = config.get("authentication", {}) + for key in ("psk", "ppk"): + items = auth.get(key) + if isinstance(items, list): + auth[key] = {item["name"]: item for item in items} + + # ------------------------------------------------------------------- + # ESP groups + # ------------------------------------------------------------------- + + def _compare_esp_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_groups = haved.get("esp_group", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("esp_group", {})): + if name in have_groups: + self.commands.append("delete vpn ipsec esp-group {0}".format(name)) + return + + want_groups = wantd.get("esp_group", {}) + names = set(want_groups) | set(have_groups) if select_all else set(want_groups) + + for name in names: + w = want_groups.get(name, {}) + h = have_groups.get(name, {}) + if w == h: + continue + + if name in have_groups and name not in want_groups: + # only reached when select_all (overridden): item entirely + # absent from want -> delete wholesale + self.commands.append("delete vpn ipsec esp-group {0}".format(name)) + continue + + if name not in have_groups: + self.addcmd({"name": name}, "esp_group", False) + + for field in ("mode", "pfs", "lifetime", "life_bytes", "life_packets"): + self._cmp_scalar( + w, + h, + field, + {"name": name}, + "esp_group.{0}".format(field), + reconcile, + ) + for field in ("compression", "disable_rekey"): + self._cmp_bool( + w, + h, + field, + {"name": name}, + "esp_group.{0}".format(field), + reconcile, + ) + + self._compare_proposals( + w.get("proposal", {}), + h.get("proposal", {}), + {"name": name}, + "esp_group.proposal", + "esp_group.proposal.encryption", + "esp_group.proposal.hash", + None, + reconcile, + ) + + # ------------------------------------------------------------------- + # IKE groups + # ------------------------------------------------------------------- + + def _compare_ike_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_groups = haved.get("ike_group", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("ike_group", {})): + if name in have_groups: + self.commands.append("delete vpn ipsec ike-group {0}".format(name)) + return + + want_groups = wantd.get("ike_group", {}) + names = set(want_groups) | set(have_groups) if select_all else set(want_groups) + + for name in names: + w = want_groups.get(name, {}) + h = have_groups.get(name, {}) + if w == h: + continue + + if name in have_groups and name not in want_groups: + self.commands.append("delete vpn ipsec ike-group {0}".format(name)) + continue + + if name not in have_groups: + self.addcmd({"name": name}, "ike_group", False) + + self._cmp_scalar( + w, + h, + "key_exchange", + {"name": name}, + "ike_group.key_exchange", + reconcile, + ) + for field in ("close_action", "lifetime", "mode"): + self._cmp_scalar( + w, + h, + field, + {"name": name}, + "ike_group.{0}".format(field), + reconcile, + ) + for field in ("disable_mobike", "ikev2_reauth"): + self._cmp_bool( + w, + h, + field, + {"name": name}, + "ike_group.{0}".format(field), + reconcile, + ) + + w_dpd = w.get("dead_peer_detection", {}) + h_dpd = h.get("dead_peer_detection", {}) + for field in ("action", "interval", "timeout"): + self._cmp_scalar( + w_dpd, + h_dpd, + field, + {"name": name}, + "ike_group.dead_peer_detection.{0}".format(field), + reconcile, + ) + + self._compare_proposals( + w.get("proposal", {}), + h.get("proposal", {}), + {"name": name}, + "ike_group.proposal", + "ike_group.proposal.encryption", + "ike_group.proposal.hash", + "ike_group.proposal.dh_group", + reconcile, + ) + + # ------------------------------------------------------------------- + # Proposals (shared by esp_group / ike_group) + # ------------------------------------------------------------------- + + def _compare_proposals( + self, + want_props, + have_props, + group_ctx, + bare_parser, + encryption_parser, + hash_parser, + dh_group_parser, + reconcile, + ): + # a proposal collection lives entirely inside an already-selected + # group -- once that group is in scope, its own proposals always + # get full reconciliation under replaced/overridden (never a + # separate select_all concern of their own). + ids = set(want_props) | set(have_props) if reconcile else set(want_props) + for pid in ids: + w = want_props.get(pid, {}) + h = have_props.get(pid, {}) + if w == h: + continue + + if pid in have_props and pid not in want_props: + self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, True) + continue + + if pid not in have_props: + self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, False) + + ctx = dict(group_ctx, proposal_id=pid) + self._cmp_scalar(w, h, "encryption", ctx, encryption_parser, reconcile) + self._cmp_scalar(w, h, "hash", ctx, hash_parser, reconcile) + if dh_group_parser: + self._cmp_scalar(w, h, "dh_group", ctx, dh_group_parser, reconcile) + + # ------------------------------------------------------------------- + # Profiles + # ------------------------------------------------------------------- + + def _compare_profiles(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_profiles = haved.get("profile", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("profile", {})): + if name in have_profiles: + self.commands.append("delete vpn ipsec profile {0}".format(name)) + return + + want_profiles = wantd.get("profile", {}) + names = set(want_profiles) | set(have_profiles) if select_all else set(want_profiles) + + for name in names: + w = want_profiles.get(name, {}) + h = have_profiles.get(name, {}) + if w == h: + continue + + if name in have_profiles and name not in want_profiles: + self.commands.append("delete vpn ipsec profile {0}".format(name)) + continue + + if name not in have_profiles: + self.addcmd({"name": name}, "profile", False) + + ctx = {"name": name} + w_auth = w.get("authentication", {}) + h_auth = h.get("authentication", {}) + self._cmp_scalar( + w_auth, + h_auth, + "mode", + ctx, + "profile.authentication.mode", + reconcile, + ) + self._cmp_scalar( + w_auth, + h_auth, + "pre_shared_secret", + ctx, + "profile.authentication.pre_shared_secret", + reconcile, + ) + self._cmp_scalar(w, h, "esp_group", ctx, "profile.esp_group", reconcile) + self._cmp_scalar(w, h, "ike_group", ctx, "profile.ike_group", reconcile) + self._cmp_bool(w, h, "disable", ctx, "profile.disable", reconcile) + + w_tunnels = set(w.get("bind_tunnel") or []) + h_tunnels = set(h.get("bind_tunnel") or []) + for tun in w_tunnels - h_tunnels: + self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", False) + if reconcile: + for tun in h_tunnels - w_tunnels: + self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", True) + + # ------------------------------------------------------------------- + # PSKs + # ------------------------------------------------------------------- + + def _compare_psks(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_psks = haved.get("authentication", {}).get("psk", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("authentication", {}).get("psk", {})): + if name in have_psks: + self.commands.append( + "delete vpn ipsec authentication psk {0}".format(name), + ) + return + + want_psks = wantd.get("authentication", {}).get("psk", {}) + names = set(want_psks) | set(have_psks) if select_all else set(want_psks) + + for name in names: + w = want_psks.get(name, {}) + h = have_psks.get(name, {}) + if w == h: + continue + + if name in have_psks and name not in want_psks: + self.commands.append("delete vpn ipsec authentication psk {0}".format(name)) + continue + + if name not in have_psks: + self.addcmd({"name": name}, "authentication.psk", False) + + ctx = {"name": name} + self._cmp_scalar(w, h, "secret", ctx, "authentication.psk.secret", reconcile) + self._cmp_scalar( + w, + h, + "secret_type", + ctx, + "authentication.psk.secret_type", + reconcile, + ) + + w_ids = set(w.get("id") or []) + h_ids = set(h.get("id") or []) + for i in w_ids - h_ids: + self.addcmd(dict(ctx, id=i), "authentication.psk.id", False) + if reconcile: + for i in h_ids - w_ids: + self.addcmd(dict(ctx, id=i), "authentication.psk.id", True) + + w_dhcp = set(w.get("dhcp_interface") or []) + h_dhcp = set(h.get("dhcp_interface") or []) + for i in w_dhcp - h_dhcp: + self.addcmd(dict(ctx, dhcp_interface=i), "authentication.psk.dhcp_interface", False) + if reconcile: + for i in h_dhcp - w_dhcp: + self.addcmd( + dict(ctx, dhcp_interface=i), + "authentication.psk.dhcp_interface", + True, + ) + + def _compare_ppks(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_ppks = haved.get("authentication", {}).get("ppk", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("authentication", {}).get("ppk", {})): + if name in have_ppks: + self.commands.append( + "delete vpn ipsec authentication ppk {0}".format(name), + ) + return + + want_ppks = wantd.get("authentication", {}).get("ppk", {}) + names = set(want_ppks) | set(have_ppks) if select_all else set(want_ppks) + + for name in names: + w = want_ppks.get(name, {}) + h = have_ppks.get(name, {}) + if w == h: + continue + + if name in have_ppks and name not in want_ppks: + self.commands.append("delete vpn ipsec authentication ppk {0}".format(name)) + continue + + if name not in have_ppks: + self.addcmd({"name": name}, "authentication.ppk", False) + + ctx = {"name": name} + self._cmp_scalar(w, h, "secret", ctx, "authentication.ppk.secret", reconcile) + self._cmp_scalar( + w, + h, + "secret_type", + ctx, + "authentication.ppk.secret_type", + reconcile, + ) + + w_ids = set(w.get("id") or []) + h_ids = set(h.get("id") or []) + for i in w_ids - h_ids: + self.addcmd(dict(ctx, id=i), "authentication.ppk.id", False) + if reconcile: + for i in h_ids - w_ids: + self.addcmd(dict(ctx, id=i), "authentication.ppk.id", True) + + # ------------------------------------------------------------------- + # Top-level scalar / list / bool fields + # + # NOTE: these are all direct fields of the single top-level config + # object, not named collections -- there is no "item entirely absent + # from want" concept here, only "field omitted from want". So only + # `reconcile` applies; `select_all` is irrelevant at this level (it's + # accepted for a consistent call signature but unused). + # ------------------------------------------------------------------- + + def _compare_top_level(self, wantd, haved, select_all, reconcile, scoped_delete=None): + if scoped_delete is not None: + # Principle: naming a parameter under scoped `deleted` means + # "delete this specific value" -- a scalar/bool key present + # (regardless of value) signals whole-field removal; a list + # value present means "delete exactly these elements", not + # the whole list, mirroring vyos_vrf's bind_to_all precedent + # extended consistently to list- and nested-dict-shaped + # fields. + if "disable_uniqreqids" in scoped_delete and haved.get("disable_uniqreqids"): + self.commands.append("delete vpn ipsec disable-uniqreqids") + + h_ifaces = set(haved.get("interface") or []) + for i in set(scoped_delete.get("interface") or []) & h_ifaces: + self.addcmd({"interface": i}, "interface", True) + + s_log = scoped_delete.get("log", {}) + h_log = haved.get("log", {}) + if "level" in s_log and "level" in h_log: + self.addcmd({"level": h_log["level"]}, "log.level", True) + h_sub = set(h_log.get("subsystem") or []) + for s in set(s_log.get("subsystem") or []) & h_sub: + self.addcmd({"subsystem": s}, "log.subsystem", True) + + s_opt = scoped_delete.get("options", {}) + h_opt = haved.get("options", {}) + for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"): + if field in s_opt and h_opt.get(field): + self.addcmd({}, "options.{0}".format(field), True) + if "interface" in s_opt and "interface" in h_opt: + self.addcmd({"interface": h_opt["interface"]}, "options.interface", True) + + s_retrans = s_opt.get("retransmission", {}) + h_retrans = h_opt.get("retransmission", {}) + for field in ("attempts", "base", "timeout"): + if field in s_retrans and field in h_retrans: + self.addcmd( + {field: h_retrans[field]}, + "options.retransmission.{0}".format(field), + True, + ) + return + + self._cmp_bool(wantd, haved, "disable_uniqreqids", {}, "disable_uniqreqids", reconcile) + + w_ifaces = set(wantd.get("interface") or []) + h_ifaces = set(haved.get("interface") or []) + for i in w_ifaces - h_ifaces: + self.addcmd({"interface": i}, "interface", False) + if reconcile: + for i in h_ifaces - w_ifaces: + self.addcmd({"interface": i}, "interface", True) + + w_log = wantd.get("log", {}) + h_log = haved.get("log", {}) + self._cmp_scalar(w_log, h_log, "level", {}, "log.level", reconcile) + w_sub = set(w_log.get("subsystem") or []) + h_sub = set(h_log.get("subsystem") or []) + for s in w_sub - h_sub: + self.addcmd({"subsystem": s}, "log.subsystem", False) + if reconcile: + for s in h_sub - w_sub: + self.addcmd({"subsystem": s}, "log.subsystem", True) + + w_opt = wantd.get("options", {}) + h_opt = haved.get("options", {}) + for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"): + self._cmp_bool(w_opt, h_opt, field, {}, "options.{0}".format(field), reconcile) + self._cmp_scalar(w_opt, h_opt, "interface", {}, "options.interface", reconcile) + + w_retrans = w_opt.get("retransmission", {}) + h_retrans = h_opt.get("retransmission", {}) + for field in ("attempts", "base", "timeout"): + self._cmp_scalar( + w_retrans, + h_retrans, + field, + {}, + "options.retransmission.{0}".format(field), + reconcile, + ) + + # ------------------------------------------------------------------- + # Field-level helpers (mirrors vyos_nat's _cmp_scalar / _cmp_bool) + # ------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif reconcile and h is not None: + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False): + # An explicitly-specified value (even False) is always enforced, + # regardless of state -- that's the user directly saying what + # they want. An OMITTED field is only enforced (i.e. deleted if + # currently True) under full reconciliation (replaced/overridden). + # Under merged, an omitted field is left alone -- protected + # further upstream by dict_merge backfilling `want` from `have` + # before this is ever reached, but this still needs to be correct + # in isolation (e.g. for a field nested inside a dict that wasn't + # part of the dict_merge'd top-level structure). + explicit = field in want + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h and (w or explicit or reconcile): + self.addcmd(dict(ctx), parser, not w) diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..65232ed7 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,355 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos_vpn_ipsec_s2s config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. + +Mirrors vyos_vpn_ipsec's config.py exactly -- same list-to-dict +conversion + explicit per-state branching, same select_all/reconcile +two-flag design for the replaced/overridden distinction, same scoped +deleted handling. See that file's own docstring for the full state +semantics; the summary: + + - merged: only items/fields named in `want` are touched. + - replaced: only items NAMED in `want` are touched, but each named + item is fully reconciled (omitted fields removed). + - overridden: every item is in scope, including ones absent from + `want` -- those get deleted wholesale. Named items + reconciled the same way as replaced. + - deleted: bare (no config) deletes everything; a scoped config + deletes only what's named, down to individual list + elements. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( + Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2s(ResourceModule): + """ + The vyos_vpn_ipsec_s2s config class + """ + + def __init__(self, module): + super(Vpn_ipsec_s2s, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vpn_ipsec_s2s", + tmplt=Vpn_ipsec_s2sTemplate(), + ) + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) or {} + haved = deepcopy(self.have) or {} + + for entry in (wantd, haved): + self._list_to_dict(entry) + + scoped_delete = None + if self.state == "deleted": + if wantd: + scoped_delete = wantd + wantd = {} + + if self.state == "merged": + # list_to_dict() above must run BEFORE this -- dict_merge + # concatenates raw lists rather than merging matching items + # by key, so it only does the right thing once both sides + # are already name-keyed dicts. + wantd = dict_merge(haved, wantd) + + select_all = self.state in ("overridden", "deleted") + reconcile = self.state in ("replaced", "overridden", "deleted") + + self._compare_peers(wantd, haved, select_all, reconcile, scoped_delete) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------- + # List -> name-keyed dict conversion + # ------------------------------------------------------------------- + + def _list_to_dict(self, config): + peers = config.get("peer") + if isinstance(peers, list): + config["peer"] = {p["name"]: p for p in peers} + for peer in config["peer"].values(): + if isinstance(peer.get("tunnel"), list): + peer["tunnel"] = {t["tunnel_id"]: t for t in peer["tunnel"]} + + # ------------------------------------------------------------------- + # Peers + # ------------------------------------------------------------------- + + def _compare_peers(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_peers = haved.get("peer", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("peer", {})): + if name in have_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + return + + want_peers = wantd.get("peer", {}) + names = set(want_peers) | set(have_peers) if select_all else set(want_peers) + + for name in names: + w = want_peers.get(name, {}) + h = have_peers.get(name, {}) + if w == h: + continue + + if name in have_peers and name not in want_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + continue + + if name not in have_peers: + self.addcmd({"name": name}, "peer", False) + + ctx = {"name": name} + self._cmp_bool(w, h, "disable", ctx, "peer.disable", reconcile) + + w_auth = w.get("authentication", {}) + h_auth = h.get("authentication", {}) + for field in ("local_id", "remote_id", "mode"): + self._cmp_scalar( + w_auth, + h_auth, + field, + ctx, + "peer.authentication.{0}".format(field), + reconcile, + ) + self._cmp_bool( + w_auth, + h_auth, + "use_x509_id", + ctx, + "peer.authentication.use_x509_id", + reconcile, + ) + + w_ppk = w_auth.get("ppk", {}) + h_ppk = h_auth.get("ppk", {}) + self._cmp_scalar(w_ppk, h_ppk, "id", ctx, "peer.authentication.ppk.id", reconcile) + self._cmp_bool( + w_ppk, + h_ppk, + "required", + ctx, + "peer.authentication.ppk.required", + reconcile, + ) + + w_rsa = w_auth.get("rsa", {}) + h_rsa = h_auth.get("rsa", {}) + for field in ("local_key", "remote_key", "passphrase"): + self._cmp_scalar( + w_rsa, + h_rsa, + field, + ctx, + "peer.authentication.rsa.{0}".format(field), + reconcile, + ) + + w_x509 = w_auth.get("x509", {}) + h_x509 = h_auth.get("x509", {}) + for field in ("certificate", "passphrase"): + self._cmp_scalar( + w_x509, + h_x509, + field, + ctx, + "peer.authentication.x509.{0}".format(field), + reconcile, + ) + w_ca = set(w_x509.get("ca_certificate") or []) + h_ca = set(h_x509.get("ca_certificate") or []) + for cert in w_ca - h_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + False, + ) + if reconcile: + for cert in h_ca - w_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + True, + ) + + for field in ( + "childless", + "connection_type", + "default_esp_group", + "description", + "dhcp_interface", + "ike_group", + "ikev2_reauth", + "local_address", + ): + self._cmp_scalar(w, h, field, ctx, "peer.{0}".format(field), reconcile) + self._cmp_bool( + w, + h, + "force_udp_encapsulation", + ctx, + "peer.force_udp_encapsulation", + reconcile, + ) + self._cmp_scalar(w, h, "replay_window", ctx, "peer.replay_window", reconcile) + + w_remote_addr = set(w.get("remote_address") or []) + h_remote_addr = set(h.get("remote_address") or []) + for addr in w_remote_addr - h_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", False) + if reconcile: + for addr in h_remote_addr - w_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", True) + + w_virt_addr = set(w.get("virtual_address") or []) + h_virt_addr = set(h.get("virtual_address") or []) + for addr in w_virt_addr - h_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", False) + if reconcile: + for addr in h_virt_addr - w_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", True) + + self._compare_tunnels(w.get("tunnel", {}), h.get("tunnel", {}), ctx, reconcile) + self._compare_vti(w.get("vti", {}), h.get("vti", {}), ctx, reconcile) + + # ------------------------------------------------------------------- + # Tunnels (nested collection within a peer) + # ------------------------------------------------------------------- + + def _compare_tunnels(self, want_tunnels, have_tunnels, peer_ctx, reconcile): + # A tunnel collection lives entirely inside an already-selected + # peer -- once that peer is in scope, its own tunnels always get + # full reconciliation under replaced/overridden, matching how + # esp_group/ike_group's own nested proposals behave in the + # profile module. + ids = set(want_tunnels) | set(have_tunnels) if reconcile else set(want_tunnels) + for tid in ids: + w = want_tunnels.get(tid, {}) + h = have_tunnels.get(tid, {}) + if w == h: + continue + + if tid in have_tunnels and tid not in want_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", True) + continue + + if tid not in have_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", False) + + ctx = dict(peer_ctx, tunnel_id=tid) + self._cmp_bool(w, h, "disable", ctx, "peer.tunnel.disable", reconcile) + for field in ("esp_group", "protocol"): + self._cmp_scalar(w, h, field, ctx, "peer.tunnel.{0}".format(field), reconcile) + self._cmp_scalar(w, h, "priority", ctx, "peer.tunnel.priority", reconcile) + + for side in ("local", "remote"): + w_side = w.get(side, {}) + h_side = h.get(side, {}) + self._cmp_scalar( + w_side, + h_side, + "port", + ctx, + "peer.tunnel.{0}.port".format(side), + reconcile, + ) + w_prefix = set(w_side.get("prefix") or []) + h_prefix = set(h_side.get("prefix") or []) + for p in w_prefix - h_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + False, + ) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + True, + ) + + # ------------------------------------------------------------------- + # VTI (nested dict within a peer, not a collection) + # ------------------------------------------------------------------- + + def _compare_vti(self, w_vti, h_vti, peer_ctx, reconcile): + for field in ("bind", "esp_group"): + self._cmp_scalar(w_vti, h_vti, field, peer_ctx, "peer.vti.{0}".format(field), reconcile) + + w_ts = w_vti.get("traffic_selector", {}) + h_ts = h_vti.get("traffic_selector", {}) + for side in ("local", "remote"): + w_prefix = set(w_ts.get(side, {}).get("prefix") or []) + h_prefix = set(h_ts.get(side, {}).get("prefix") or []) + parser = "peer.vti.traffic_selector.{0}.prefix".format(side) + for p in w_prefix - h_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, False) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, True) + + # ------------------------------------------------------------------- + # Field-level helpers (mirrors vyos_vpn_ipsec's own) + # ------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif reconcile and h is not None: + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False): + explicit = field in want + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h and (w or explicit or reconcile): + self.addcmd(dict(ctx), parser, not w) diff --git a/plugins/module_utils/network/vyos/config/vrf/__init__.py b/plugins/module_utils/network/vyos/config/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/config/vrf/vrf.py b/plugins/module_utils/network/vyos/config/vrf/vrf.py new file mode 100644 index 00000000..a55073b0 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vrf/vrf.py @@ -0,0 +1,307 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +""" +The vyos_vrf config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.bgp_global.bgp_global import ( + Bgp_global, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv2.ospfv2 import ( + Ospfv2, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.ospfv3.ospfv3 import ( + Ospfv3, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.static_routes.static_routes import ( + Static_routes, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import Facts +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import ( + VrfTemplate, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import combine +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version + + +class Vrf(ResourceModule): + """ + The vyos_vrf config class + """ + + def __init__(self, module): + super(Vrf, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vrf", + tmplt=VrfTemplate(), + ) + self.parsers = [ + "bind_to_all", + ] + + def _validate_template(self): + version = get_os_version(self._module) + if LooseVersion(version) >= LooseVersion("1.4"): + self._tmplt = VrfTemplate() + else: + self._module.fail_json(msg="VRF is not supported in this version of VyOS") + + def parse(self): + """override parse to check template""" + self._validate_template() + return super().parse() + + def get_parser(self, name): + """get_parsers""" + self._validate_template() + return super().get_parser(name) + + def execute_module(self): + """Execute the module + + :rtype: A dictionary + :returns: The result from module execution + """ + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + + return self.result + + def generate_commands(self): + """Generate configuration commands to send based on + want, have and desired state. + """ + wantd = {} + haved = {} + wantd = deepcopy(self.want) + haved = deepcopy(self.have) + + # if state is merged, merge want onto have and then compare + if self.state in ["merged", "replaced"]: + + wantd = combine(haved, wantd, recursive=True) + + # if state is deleted, delete and empty out wantd + if self.state == "deleted": + w = deepcopy(wantd) + if w == {} and haved != {}: + self.commands = ["delete vrf"] + return + for k, want in w.items(): + if not (k in haved and haved[k]): + del wantd[k] + else: + if isinstance(want, list): + for entry in want: + wname = entry.get("name") + haved["instances"] = [ + i for i in haved.get("instances", []) if i.get("name") != wname + ] + self.commands.append("delete vrf name {}".format(wname)) + else: + self.commands.append("delete vrf {}".format(k.replace("_", "-"))) + del wantd[k] + + if self.state == "overridden": + w = deepcopy(wantd) + h = deepcopy(haved) + for k, want in w.items(): + if k in haved and haved[k] != want: + if isinstance(want, list): + for entry in want: + wname = entry.get("name") + hdict = next( + (inst for inst in haved["instances"] if inst["name"] == wname), + None, + ) + wantc = self._canonicalise(entry) + havec = self._canonicalise(hdict or {}) + + if wantc != havec: + haved["instances"] = [ + i for i in haved.get("instances", []) if i.get("name") != wname + ] + self.commands.append("delete vrf name {}".format(wname)) + self.commands.append("commit") + + for k, want in wantd.items(): + if isinstance(want, list): + self._compare_instances(want=want, have=haved.pop(k, {})) + self.compare( + parsers=self.parsers, + want={k: want}, + have={k: haved.pop(k, {})}, + ) + + def _compare_instances(self, want, have): + """Compare the instances of the VRF""" + parsers = [ + "table_id", + "vni", + "description", + "disable_vrf", + ] + + for entry in want: + h = {} + wname = entry.get("name") + h = { + k: v + for vrf in have + if vrf.get("name") == wname + for k, v in vrf.items() + if k != "address_family" + } + self.compare(parsers=parsers, want=entry, have=h) + + if "address_family" in entry: + wafi = {"name": wname, "address_family": entry.get("address_family", [])} + hdict = next((d for d in have if d.get("name") == wname), None) + + hafi = { + "name": (hdict or {"name": wname})["name"], + "address_family": hdict.get("address_family", []) if hdict else [], + } + + self._compare_addr_family(wafi, hafi) + + if "protocols" in entry: + for protocol_name in entry["protocols"]: + + w_p_dict = entry["protocols"][protocol_name] + + h_p_dict = next( + ( + v.get("protocols", {}).get(protocol_name, {}) + for v in have + if v.get("name") == wname + ), + {}, + ) + if protocol_name == "bgp": + bgp_module = Bgp_global(self._module) + bgp_module._validate_template() + bgp_module.want = w_p_dict + bgp_module.have = h_p_dict + bgp_module.generate_commands() + protocol_commands = bgp_module.commands + elif protocol_name == "ospf": + ospfv2_module = Ospfv2(self._module) + ospfv2_module._module.params["config"] = w_p_dict + ospfv2_module.state = self.state + protocol_commands = ospfv2_module.set_config(h_p_dict) + elif protocol_name == "ospfv3": + ospfv3_module = Ospfv3(self._module) + ospfv3_module._module.params["config"] = w_p_dict + ospfv3_module.state = self.state + protocol_commands = ospfv3_module.set_config(h_p_dict) + elif protocol_name == "static": + static_routes_module = Static_routes(self._module) + static_routes_module._module.params["config"] = w_p_dict + static_routes_module.state = self.state + protocol_commands = static_routes_module.set_config(h_p_dict) + else: + self._module.fail_json( + msg="The protocol {} is not supported".format(protocol_name), + ) + self.commands.extend( + [ + cmd.replace("protocols", "vrf name " + wname + " protocols", 1) + for cmd in protocol_commands + ], + ) + + def _compare_addr_family(self, want, have): + """Compare the address families of the VRF""" + afi_parsers = [ + "disable_forwarding", + "disable_nht", + ] + + wafi = self.afi_to_list(want) + hafi = self.afi_to_list(have) + + lookup = {(d["name"], d["afi"]): d for d in hafi} + pairs = [(d1, lookup.get((d1["name"], d1["afi"]), {})) for d1 in wafi] + + for wafd, hafd in pairs: + if "route_maps" in wafd: + self._compare_route_maps(wafd, hafd) + self.compare(parsers=afi_parsers, want=wafd, have=hafd) + + def afi_to_list(self, data): + """Convert address family dict to list""" + + return [ + {"name": data["name"], **{**af, "afi": "ip" if af["afi"] == "ipv4" else af["afi"]}} + for af in data["address_family"] + ] + + def _compare_route_maps(self, wafd, hafd): + want_rms = wafd.get("route_maps", []) + have_rms = hafd.get("route_maps", []) + + for want in want_rms: + match = next( + ( + h + for h in have_rms + if h["rm_name"] == want["rm_name"] and h["protocol"] == want["protocol"] + ), + {}, + ) + base = {"name": wafd["name"], "afi": wafd["afi"]} + + self.compare( + parsers="route_maps", + want={**base, "route_maps": want}, + have={**base, "route_maps": match}, + ) + + def _canonicalise(self, obj): + if isinstance(obj, dict): + return {k: self._canonicalise(v) for k, v in obj.items()} + + if isinstance(obj, list): + if not obj: + return obj + + if not isinstance(obj[0], dict): + return sorted(obj) + + canon = [self._canonicalise(i) for i in obj] + + def sort_key(d): + for k in ("name", "address", "afi", "area_id", "neighbor_id", "dest"): + if k in d: + return d[k] + return tuple(sorted(d.items())) + + return sorted(canon, key=sort_key) + + return obj diff --git a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py index 3386bd66..dab92612 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/facts/bgp_address_family/bgp_address_family.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -28,11 +27,11 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_template from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_address_family_14 import ( Bgp_address_familyTemplate14, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_address_familyFacts(object): """The vyos bgp_address_family facts class""" diff --git a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py index dd793681..cf2e4475 100644 --- a/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py +++ b/plugins/module_utils/network/vyos/facts/bgp_global/bgp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -25,15 +24,14 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.bgp from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global import ( Bgp_globalTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.bgp_global_14 import ( Bgp_globalTemplate14, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Bgp_globalFacts(object): """The vyos bgp_global facts class""" diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 74bbda74..92c0f97a 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -6,8 +6,8 @@ The facts class for vyos this file validates each subset of facts and selectively calls the appropriate facts gathering function """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -30,6 +30,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firew from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import ( Firewall_rulesFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ha.ha import ( + HaFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.hostname.hostname import ( HostnameFacts, ) @@ -56,6 +59,9 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import ( Logging_globalFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.nat.nat import ( + NatFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import ( Ntp_globalFacts, ) @@ -80,7 +86,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( Static_routesFacts, ) - +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config) FACT_RESOURCE_SUBSETS = dict( @@ -93,6 +105,7 @@ FACT_RESOURCE_SUBSETS = dict( firewall_rules=Firewall_rulesFacts, firewall_global=Firewall_globalFacts, firewall_interfaces=Firewall_interfacesFacts, + ha=HaFacts, ospfv3=Ospfv3Facts, ospfv2=Ospfv2Facts, ospf_interfaces=Ospf_interfacesFacts, @@ -102,8 +115,12 @@ FACT_RESOURCE_SUBSETS = dict( prefix_lists=Prefix_listsFacts, logging_global=Logging_globalFacts, ntp_global=Ntp_globalFacts, + nat=NatFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, + vrf=VrfFacts, + vpn_ipsec=Vpn_ipsecFacts, + vpn_ipsec_s2s=Vpn_ipsec_s2sFacts, ) diff --git a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py index a46f8563..e13c1939 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py +++ b/plugins/module_utils/network/vyos/facts/firewall_global/firewall_global.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -77,13 +77,17 @@ class Firewall_globalFacts(object): :rtype: dictionary :returns: The generated config """ + conf = "\n".join( filter( - lambda x: ("firewall ipv6-name" and "firewall name" not in x), + lambda x: not ( + x.startswith("set firewall name") + or x.startswith("set firewall ipv6-name") + or x.startswith("set firewall ipv6 name") + ), conf, ), ) - a_lst = [ "config_trap", "validation", @@ -97,6 +101,7 @@ class Firewall_globalFacts(object): "group": self.parse_group(conf), "route_redirects": self.route_redirects(conf), "state_policy": self.parse_state_policy(conf), + "zone": self.parse_zone(conf), } firewall.update(f_sub) return firewall @@ -179,7 +184,7 @@ class Firewall_globalFacts(object): if policies: rules_lst = [] for sp in set(policies): - sp_regex = r" %s .+$" % sp + sp_regex = r"^set firewall (?:global-options )?state-policy %s .+$" % sp cfg = "\n".join(findall(sp_regex, conf, M)) obj = self.parse_policies(cfg, sp) obj["connection_type"] = sp @@ -233,7 +238,7 @@ class Firewall_globalFacts(object): if groups: rules_lst = [] for gr in set(groups): - gr_regex = r" %s .+$" % gr + gr_regex = r"^set firewall group " + type + " %s .+$" % gr cfg = "\n".join(findall(gr_regex, conf, M)) if "ipv6" in type: # fmt: off @@ -400,3 +405,120 @@ class Firewall_globalFacts(object): "twa_hazards_protection", ) return True if attrib in bool_set else False + + def parse_zone(self, conf): + """ + This function triggers the parsing of 'zone' attributes. + :param conf: configuration. + :return: generated config dictionary. + """ + cfg_dict = {} + + KEY_MAP = { + "interface": "interfaces", + "intra-zone-filtering": "intra-zone-filtering", + "from": "sources", + } + + LIST_ATTRS = { + "interfaces", + "intra_zone_filtering", + "sources", + } + + for line in conf.splitlines(): + + m = search( + r"^set firewall zone (?P<zone>\S+)\s+(?P<attr>[a-z-]+)(?:\s+(?P<value>'[^']+'|[^\n]+))?$", + line, + ) + if not m: + continue + + zone_name = m.group("zone") + raw_attr = m.group("attr").replace("-", "_") + value = m.group("value") + + if value is None: + value = True + else: + value = value.strip("'") + + # VyOS 1.5.0 GA wraps 'interface' under a new 'member' node: + # "set firewall zone <name> member interface <ifname>". Unwrap + # it here so it lands in the same 'interfaces' list as the + # pre-1.5.0 bare "interface <ifname>" form. No version check + # needed -- 1.4.x/1.5-rolling configs never emit 'member'. + if raw_attr == "member" and isinstance(value, str) and value.startswith("interface "): + raw_attr = "interface" + value = value.split(None, 1)[1].strip("'") + + zone = cfg_dict.setdefault(zone_name, {"name": zone_name}) + + attr = KEY_MAP.get(raw_attr, raw_attr) + + if attr in LIST_ATTRS: + if attr == "intra_zone_filtering": + izf = zone.setdefault(attr, {}) + izf_attr = self._parse_izf(value) + for k, v in izf_attr.items(): + if isinstance(v, dict): + izf.setdefault(k, {}).update(v) + else: + izf[k] = v + elif attr == "sources": + self._parse_sources(zone, value) + else: + zone.setdefault(attr, []).append(value) + else: + zone[attr] = value + + return list(cfg_dict.values()) + + def _parse_izf(self, value): + + tokens = value.replace("'", "").split() + + result = {} + + key = tokens[0].replace("-", "_") + + if len(tokens) == 2: + result[key] = tokens[1] + + elif len(tokens) >= 3: + subkey = tokens[1].replace("-", "_") + result[key] = {subkey: tokens[2]} + + return result + + def _parse_sources(self, zone, value): + + tokens = value.split() + + if len(tokens) < 1: + return + + src_zone = tokens[0] + + sources = zone.setdefault("sources", []) + + entry = None + for s in sources: + if s.get("zone") == src_zone: + entry = s + break + + if entry is None: + entry = {"zone": src_zone} + sources.append(entry) + + if len(tokens) == 1: + return + + if tokens[1] == "firewall" and len(tokens) >= 4: + key = tokens[2].replace("-", "_") + val = tokens[3].strip("'") + + firewall = entry.setdefault("firewall", {}) + firewall[key] = val diff --git a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py index bac31920..34235af7 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/firewall_interfaces/firewall_interfaces.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py index a6b56345..31cc1fa5 100644 --- a/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py +++ b/plugins/module_utils/network/vyos/facts/firewall_rules/firewall_rules.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -61,12 +61,20 @@ class Firewall_rulesFacts(object): objs = [] # check 1.4+ first new_rules = True - v6_rules = findall(r"^set firewall ipv6 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + v6_rules = findall( + r"^set firewall ipv6 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", + data, + M, + ) if not v6_rules: v6_rules = findall(r"^set firewall ipv6-name (?:\'*)(\S+)(?:\'*)", data, M) if v6_rules: new_rules = False - v4_rules = findall(r"^set firewall ipv4 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", data, M) + v4_rules = findall( + r"^set firewall ipv4 (name|forward|input|output) (?:\'*)(\S+)(?:\'*)", + data, + M, + ) if not v4_rules: v4_rules = findall(r"^set firewall name (?:\'*)(\S+)(?:\'*)", data, M) if v4_rules: @@ -171,12 +179,18 @@ class Firewall_rulesFacts(object): :returns: The generated config """ conf = "\n".join(filter(lambda x: x, conf)) - a_lst = ["description", "default_action", "default_jump_target", "enable_default_log", "default_log"] + a_lst = [ + "description", + "default_action", + "default_jump_target", + "enable_default_log", + "default_log", + ] config = self.parse_attr(conf, a_lst, match) if not config: config = {} - if 'default_log' in config: - config['enable_default_log'] = config.pop('default_log') + if "default_log" in config: + config["enable_default_log"] = config.pop("default_log") config["rules"] = self.parse_rules_lst(conf) return config @@ -219,6 +233,7 @@ class Firewall_rulesFacts(object): "description", "icmp", "jump_target", + "offload_target", "queue", "queue_options", ] @@ -263,8 +278,8 @@ class Firewall_rulesFacts(object): found_lengths = findall(rule_regex, conf, M) if found_lengths: lengths = [] - for l in set(found_lengths): - obj = {"length": l.strip("'")} + for pplen in set(found_lengths): + obj = {"length": pplen.strip("'")} lengths.append(obj) return lengths @@ -389,21 +404,21 @@ class Firewall_rulesFacts(object): out = search(r"^.*" + regex + " (.+)", conf, M) if out: val = out.group(1).strip("'") - if attrib == 'type-name': - config['type_name'] = val - if attrib == 'code': - config['code'] = int(val) - if attrib == 'type': + if attrib == "type-name": + config["type_name"] = val + if attrib == "code": + config["code"] = int(val) + if attrib == "type": # <1.3 could be # (type), #/# (type/code) or 'type' (type_name) # recent this is only for strings if "/" in val: # type/code - (type_no, code) = val.split(".") - config['type'] = type_no - config['code'] = code + (type_no, code) = val.split("/") + config["type"] = int(type_no) + config["code"] = int(code) elif val.isnumeric(): - config['type'] = type_no + config["type"] = int(val) else: - config['type_name'] = val + config["type_name"] = val return config def parse_icmp(self, conf, attrib=None): @@ -414,7 +429,7 @@ class Firewall_rulesFacts(object): :return: generated config dictionary. """ cfg_dict = self.parse_icmp_attr(conf, "icmp") - if (len(cfg_dict) == 0): + if len(cfg_dict) == 0: cfg_dict = self.parse_icmp_attr(conf, "icmpv6") return cfg_dict @@ -458,7 +473,7 @@ class Firewall_rulesFacts(object): if not out: if attrib == "disable": out = search(r"^.*\d+" + " (disable$)", conf, M) - if attrib == 'log': + if attrib == "log": out = search(r"^.*\d+" + " (log$)", conf, M) if out: @@ -523,7 +538,7 @@ class Firewall_rulesFacts(object): if out: val = out.group(1).strip("'") if "/" in val: # number/unit - (number, unit) = val.split("/") - config['number'] = number - config['unit'] = unit + number, unit = val.split("/") + config["number"] = number + config["unit"] = unit return config diff --git a/plugins/module_utils/network/vyos/facts/ha/__init__.py b/plugins/module_utils/network/vyos/facts/ha/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/ha/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/ha/ha.py b/plugins/module_utils/network/vyos/facts/ha/ha.py new file mode 100644 index 00000000..fc559783 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/ha/ha.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos_ha fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ha.ha import ( + HaArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ha import ( + HaTemplate, +) + + +class HaFacts(object): + """The vyos_ha facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = HaArgs.argument_spec + + def get_config(self, connection): + return connection.get('show configuration commands | match "set high-availability"') + + def get_config_set(self, data, connection): + """Classify config lines into per-object buckets for isolated parsing. + + Each bucket is parsed by a single HaTemplate instance so that facts + from different objects (groups, sync-groups, virtual-servers) never + bleed into each other. + + Keys are namespaced to avoid collisions between a VRRP group and a + sync-group that share the same name (e.g. both named "g1"). + An elif chain ensures each line lands in exactly one bucket. + """ + config_dict = {} + for config_line in data.splitlines(): + vrrp_disable = re.search(r"set high-availability disable", config_line) + vrrp_snmp = re.search(r"set high-availability vrrp snmp", config_line) + vrrp_gp = re.search( + r"set high-availability vrrp global-parameters (\S+).*", + config_line, + ) + vrrp_grp = re.search(r"set high-availability vrrp group (\S+).*", config_line) + vrrp_sg = re.search(r"set high-availability vrrp sync-group (\S+).*", config_line) + vrrp_vsrv = re.search(r"set high-availability virtual-server (\S+).*", config_line) + + if vrrp_disable: + config_dict.setdefault("disable", []).append(config_line) + elif vrrp_snmp: + config_dict.setdefault("vrrp", []).append(config_line) + elif vrrp_gp: + config_dict.setdefault("global_parameters", []).append(config_line) + elif vrrp_grp: + key = "vrrp_group_{0}".format(vrrp_grp.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_sg: + key = "vrrp_sg_{0}".format(vrrp_sg.group(1)) + config_dict.setdefault(key, []).append(config_line) + elif vrrp_vsrv: + config_dict.setdefault(vrrp_vsrv.group(1), []).append(config_line) + + return list(config_dict.values()) + + def deep_merge(self, dest, src): + for key, value in src.items(): + if key in dest and isinstance(dest[key], dict) and isinstance(value, dict): + self.deep_merge(dest[key], value) + else: + dest[key] = value + return dest + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for vrrp network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = {} + + if not data: + data = self.get_config(connection) + resources = self.get_config_set(data, connection) + vrrp_facts = {"disable": False, "virtual_servers": {}, "vrrp": {}} + for resource in resources: + vrrp_parser = HaTemplate( + lines=resource, + module=self._module, + ) + objs = vrrp_parser.parse() + if "disable" in objs: + vrrp_facts["disable"] = objs["disable"] + + for section in ("virtual_servers", "vrrp"): + if section in objs: + for name, data in objs[section].items(): + if not isinstance(data, dict): + vrrp_facts[section][name] = data + continue + existing = vrrp_facts[section].get(name, {}) + vrrp_facts[section][name] = self.deep_merge(existing, data) + + ansible_facts["ansible_network_resources"].pop("ha", None) + + vrrp_facts = self.normalize_config(vrrp_facts) + + validate_parser = HaTemplate(lines=[], module=self._module) + params = utils.remove_empties( + validate_parser.validate_config( + self.argument_spec, + {"config": vrrp_facts}, + redact=True, + ), + ) + + facts["ha"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def normalize_config(self, config): + if not config: + return config + + if isinstance(config.get("virtual_servers"), dict): + config["virtual_servers"] = list(config["virtual_servers"].values()) + + vrrp = config.get("vrrp", {}) + + if isinstance(vrrp.get("groups"), dict): + vrrp["groups"] = list(vrrp["groups"].values()) + + if isinstance(vrrp.get("sync_groups"), dict): + vrrp["sync_groups"] = list(vrrp["sync_groups"].values()) + + # Normalize real_server inside each virtual_server + for vs in config.get("virtual_servers", []): + if isinstance(vs.get("real_server"), dict): + vs["real_server"] = list(vs["real_server"].values()) + + for group in vrrp.get("groups", []): + if isinstance(group.get("address"), list): + group["address"] = sorted(group["address"]) + + if isinstance(group.get("excluded_address"), list): + group["excluded_address"] = sorted(group["excluded_address"]) + + if isinstance(group.get("track", {}).get("interface"), list): + group["track"]["interface"] = sorted(group["track"]["interface"]) + + for sg in vrrp.get("sync_groups", []): + if isinstance(sg.get("member"), list): + sg["member"] = sorted(sg["member"]) + + return config diff --git a/plugins/module_utils/network/vyos/facts/hostname/hostname.py b/plugins/module_utils/network/vyos/facts/hostname/hostname.py index b4f7c529..18a66357 100644 --- a/plugins/module_utils/network/vyos/facts/hostname/hostname.py +++ b/plugins/module_utils/network/vyos/facts/hostname/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py index cd8008c6..30d619a3 100644 --- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -80,6 +79,7 @@ class InterfacesFacts(object): facts["interfaces"].append(utils.remove_empties(cfg)) ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts def render_config(self, conf): @@ -94,7 +94,7 @@ class InterfacesFacts(object): """ vif_conf = "\n".join(filter(lambda x: ("vif" in x), conf)) eth_conf = "\n".join(filter(lambda x: ("vif" not in x), conf)) - config = self.parse_attribs(["description", "speed", "mtu", "duplex"], eth_conf) + config = self.parse_attribs(["description", "speed", "mtu", "duplex", "vrf"], eth_conf) config["vifs"] = self.parse_vifs(vif_conf) return utils.remove_empties(config) diff --git a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py index 7d4d1a08..9e79e24d 100644 --- a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type @@ -20,7 +19,6 @@ import re from copy import deepcopy -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.l3_interfaces.l3_interfaces import ( @@ -139,7 +137,7 @@ class L3_interfacesFacts(object): else: config["ipv6"].append({"address": item}) - for key, value in iteritems(config): + for key, value in config.items(): if value == []: config[key] = None diff --git a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py index 8e1c8624..94c73416 100644 --- a/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/lag_interfaces/lag_interfaces.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/legacy/base.py b/plugins/module_utils/network/vyos/facts/legacy/base.py index 30978e0e..59666e19 100644 --- a/plugins/module_utils/network/vyos/facts/legacy/base.py +++ b/plugins/module_utils/network/vyos/facts/legacy/base.py @@ -11,7 +11,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type import platform import re diff --git a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py index 7a6e9b8e..40ff2ad3 100644 --- a/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py +++ b/plugins/module_utils/network/vyos/facts/lldp_global/lldp_global.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py index e029b47a..72bdf154 100644 --- a/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/lldp_interfaces/lldp_interfaces.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py index 8b60bef9..243043bc 100644 --- a/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py +++ b/plugins/module_utils/network/vyos/facts/logging_global/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -15,7 +14,6 @@ for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.logging_global.logging_global import ( @@ -24,6 +22,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.log from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global import ( Logging_globalTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.logging_global_15 import ( + Logging_globalTemplate15, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Logging_globalFacts(object): @@ -38,7 +43,7 @@ class Logging_globalFacts(object): def process_facts(self, objFinal): if objFinal: - for ke, vl in iteritems(objFinal): + for ke, vl in objFinal.items(): if ke == "files": _files = [] for k, v in vl.items(): @@ -81,8 +86,17 @@ class Logging_globalFacts(object): if not data: data = self.get_logging_data(connection) - # parse native config using the Logging_global template - logging_global_parser = Logging_globalTemplate(lines=data.splitlines(), module=self._module) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.5"): + logging_global_parser = Logging_globalTemplate15( + lines=data.splitlines(), + module=self._module, + ) + else: + logging_global_parser = Logging_globalTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = logging_global_parser.parse() ansible_facts["ansible_network_resources"].pop("logging_global", None) objs = self.process_facts(objs) diff --git a/plugins/module_utils/network/vyos/facts/nat/__init__.py b/plugins/module_utils/network/vyos/facts/nat/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/nat/nat.py b/plugins/module_utils/network/vyos/facts/nat/nat.py new file mode 100644 index 00000000..73d6238f --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/nat/nat.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.nat.nat import ( + NatArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.nat import ( + NatTemplate, +) + + +class NatFacts(object): + """The vyos nat facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = NatArgs.argument_spec + + def get_config(self, connection): + return connection.get("show configuration commands | match 'set nat'") + + def populate_facts(self, connection, ansible_facts, data=None): + facts = {} + config_lines = [] + + if not data: + data = self.get_config(connection) + + for resource in data.splitlines(): + config_lines.append(re.sub(r"'([^']*)'", r"\1", resource)) + + nat_parser = NatTemplate(lines=config_lines, module=self._module) + objs = nat_parser.parse() + objs = self._normalise(objs) + + ansible_facts["ansible_network_resources"].pop("nat", None) + + params = utils.remove_empties( + nat_parser.validate_config(self.argument_spec, {"config": objs}, redact=True), + ) + + if params.get("config"): + facts["nat"] = params["config"] + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts + + def _deep_merge(self, base, override): + for k, v in override.items(): + if k in base and isinstance(base[k], dict) and isinstance(v, dict): + self._deep_merge(base[k], v) + elif k in base and isinstance(base[k], list) and isinstance(v, list): + for entry in v: + if entry not in base[k]: + base[k].append(entry) + else: + base[k] = v + return base + + def _merge_rule_list(self, rules): + merged = {} + for item in rules: + rid = item["id"] + if rid not in merged: + merged[rid] = {"id": rid} + for k, v in item.items(): + if k == "id": + continue + if isinstance(v, list): + existing = merged[rid].setdefault(k, []) + for entry in v: + if entry not in existing: + existing.append(entry) + elif isinstance(v, dict): + merged[rid].setdefault(k, {}) + self._deep_merge(merged[rid][k], v) + else: + merged[rid][k] = v + return list(merged.values()) + + def _merge_pool_list(self, pools): + merged = {} + for item in pools: + name = item["name"] + if name not in merged: + merged[name] = {"name": name} + for k, v in item.items(): + if k == "name": + continue + if k == "range" and isinstance(v, list): + existing = merged[name].setdefault(k, []) + existing.extend(v) + if v and isinstance(v[0], dict): + merged[name][k] = self._merge_range_list(existing) + else: + merged[name][k] = list(dict.fromkeys(existing)) + elif isinstance(v, list): + merged[name].setdefault(k, []) + for val in v: + if val not in merged[name][k]: + merged[name][k].append(val) + elif isinstance(v, dict): + merged[name].setdefault(k, {}) + self._deep_merge(merged[name][k], v) + else: + merged[name][k] = v + return list(merged.values()) + + def _merge_range_list(self, ranges): + """Merge external pool range entries by value, preserving seq.""" + merged = {} + for entry in ranges: + if isinstance(entry, dict): + key = entry.get("value") or entry.get("address", "") + if not key: + continue + if key not in merged: + merged[key] = {"value": key} + if entry.get("seq"): + merged[key]["seq"] = entry["seq"] + else: + if entry not in merged: + merged[entry] = {"value": entry} + return list(merged.values()) + + def _normalise(self, objs): + for nat_type in ["nat", "nat64", "nat66"]: + nat = objs.get(nat_type) + if not nat: + continue + + for section in ["destination", "source", "static", "cgnat"]: + if section not in nat: + continue + rules = nat[section].get("rule") + if isinstance(rules, list): + nat[section]["rule"] = self._merge_rule_list(rules) + nat[section]["rule"].sort(key=lambda x: x.get("id", 0)) + + if "cgnat" in nat and "pool" in nat["cgnat"]: + pool = nat["cgnat"]["pool"] + for ptype in ["external", "internal"]: + if ptype in pool and isinstance(pool[ptype], list): + pool[ptype] = self._merge_pool_list(pool[ptype]) + + if nat_type == "nat64": + for rule in nat.get("source", {}).get("rule", []): + pools = rule.get("translation", {}).get("pool") + if pools and isinstance(pools, list): + rule["translation"]["pool"] = self._merge_rule_list(pools) + rule["translation"]["pool"].sort(key=lambda x: x.get("id", 0)) + + self._cast_ports(objs) + return objs + + def _cast_ports(self, obj): + """Recursively cast known integer port/seq fields to str.""" + if isinstance(obj, dict): + for k, v in obj.items(): + if k in ("port", "seq") and isinstance(v, int): + obj[k] = str(v) + else: + self._cast_ports(v) + elif isinstance(obj, list): + for item in obj: + self._cast_ports(item) diff --git a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py index 880bc79f..a6d6892f 100644 --- a/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py +++ b/plugins/module_utils/network/vyos/facts/ntp_global/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py index 852e1da7..1a9a687d 100644 --- a/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/ospf_interfaces/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -23,17 +22,16 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.osp Ospf_interfacesArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces import ( - Ospf_interfacesTemplate + Ospf_interfacesTemplate, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.ospf_interfaces_14 import ( - Ospf_interfacesTemplate14 + Ospf_interfacesTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, ) - from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version -from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import LooseVersion - class Ospf_interfacesFacts(object): """The vyos ospf_interfaces facts class""" @@ -54,7 +52,9 @@ class Ospf_interfacesFacts(object): for config_line in data.splitlines(): ospf_int = re.search(r"set protocols (?:ospf|ospfv3) interface (\S+).*", config_line) if ospf_int: - config_dict[ospf_int.group(1)] = config_dict.get(ospf_int.group(1), "") + config_line + "\n" + config_dict[ospf_int.group(1)] = ( + config_dict.get(ospf_int.group(1), "") + config_line + "\n" + ) return list(config_dict.values()) def get_config_set_1_2(self, data): @@ -63,12 +63,15 @@ class Ospf_interfacesFacts(object): config_set = [] int_string = "" for config_line in data.splitlines(): - ospf_int = re.search(r"set interfaces \S+ (\S+) .*", config_line) + ospf_int_raw = re.findall(r"^set interfaces \S+ (\S+)", config_line, re.M) + ospf_int_vif = re.findall(r"^set interfaces \S+ (\S+) vif (\d+)", config_line, re.M) + + ospf_int = ospf_int_raw + ospf_int_vif if ospf_int: - if ospf_int.group(1) not in interface_list: + if ospf_int not in interface_list: if int_string: config_set.append(int_string) - interface_list.append(ospf_int.group(1)) + interface_list.append(ospf_int) int_string = "" int_string = int_string + config_line + "\n" if int_string: @@ -115,7 +118,6 @@ class Ospf_interfacesFacts(object): if key in objs and objs[key]: objs[key] = list(objs[key].values()) ospf_interfaces_facts.append(objs) - ansible_facts["ansible_network_resources"].pop("ospf_interfaces", None) facts = {"ospf_interfaces": []} params = utils.remove_empties( @@ -123,11 +125,10 @@ class Ospf_interfacesFacts(object): self.argument_spec, {"config": ospf_interfaces_facts}, redact=True, - ) + ), ) if params.get("config"): for cfg in params["config"]: facts["ospf_interfaces"].append(utils.remove_empties(cfg)) ansible_facts["ansible_network_resources"].update(facts) - return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py index bdc7c9f8..056f949d 100644 --- a/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py +++ b/plugins/module_utils/network/vyos/facts/ospfv2/ospfv2.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -22,6 +22,10 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common i from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv2.ospfv2 import ( Ospfv2Args, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Ospfv2Facts(object): @@ -99,9 +103,9 @@ class Ospfv2Facts(object): config["areas"] = self.parse_attrib_list(conf, "area", "area_id") config["parameters"] = self.parse_attrib(conf, "parameters", "parameters") config["neighbor"] = self.parse_attrib_list(conf, "neighbor", "neighbor_id") - config["passive_interface"] = self.parse_leaf_list(conf, "passive-interface") + config["passive_interface"] = self.parse_passive(conf, "passive-interface") config["redistribute"] = self.parse_attrib_list(conf, "redistribute", "route_type") - config["passive_interface_exclude"] = self.parse_leaf_list( + config["passive_interface_exclude"] = self.parse_passive( conf, "passive-interface-exclude", ) @@ -159,6 +163,29 @@ class Ospfv2Facts(object): lst.sort() return lst + def parse_passive(self, conf, attrib): + """ + This function forms the regex to fetch the listed attributes + from the configuration data + :param conf: configuration data + :param attrib: attribute name + :return: generated rule list configuration + """ + lst = [] + items = [] + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + if attrib == "passive-interface-exclude": + items = findall("^interface (?:'*)(\\S+)(?:'*) passive disable$", conf, M) + else: + items = findall("^interface (?:'*)(\\S+)(?:'*) passive$", conf, M) + + items += findall(r"^" + attrib + " (?:'*)(\\S+)(?:'*)", conf, M) + if items: + for i in set(items): + lst.append(i.strip("'")) + lst.sort() + return lst + def parse_distance(self, conf, attrib=None): """ This function triggers the parsing of 'distance' attributes @@ -389,13 +416,13 @@ class Ospfv2Facts(object): :param match: parent node/attribute name. :return: generated config dictionary. """ - config = {} for attrib in attr_list: regex = self.map_regex(attrib) if match: regex = match.replace("_", "-") + " " + regex + if conf: if self.is_bool(attrib): out = conf.find(attrib.replace("_", "-")) @@ -403,13 +430,13 @@ class Ospfv2Facts(object): if match: if attrib == "set" and conf.find(match) >= 1: config[attrib] = True - en = conf.find(match + " 'enable'") + en = conf.find(match + " enable") != -1 if out >= 1: if dis >= 1: config[attrib] = False else: config[attrib] = True - elif match and en >= 1: + elif match and en: config[attrib] = True else: out = search(r"^.*" + regex + " (.+)", conf, M) diff --git a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py index 547ff793..5fd9e627 100644 --- a/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py +++ b/plugins/module_utils/network/vyos/facts/ospfv3/ospfv3.py @@ -9,8 +9,8 @@ It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ -from __future__ import absolute_import, division, print_function +from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -22,6 +22,10 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common i from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.ospfv3.ospfv3 import ( Ospfv3Args, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Ospfv3Facts(object): @@ -100,6 +104,9 @@ class Ospfv3Facts(object): for item in set(items): i_regex = r" %s .+$" % item cfg = "\n".join(findall(i_regex, conf, M)) + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + cfg14 = findall(r"(interface .+) area '%s'$" % item, conf, M) + cfg += "\n " + item + " " + " ".join(cfg14) if attrib == "area": obj = self.parse_area(cfg, item) else: @@ -121,6 +128,8 @@ class Ospfv3Facts(object): rule = self.parse_attrib(conf, "area_id", match=area_id) r_sub = {"range": self.parse_attrib_list(conf, "range", "address")} rule.update(r_sub) + r_int = {"interface": self.parse_attrib_list(conf, "interface", "name")} + rule.update(r_int) return rule def parse_attrib(self, conf, param, match=None): @@ -133,6 +142,7 @@ class Ospfv3Facts(object): "area_id": ["export_list", "import_list"], "redistribute": ["route_map"], "range": ["advertise", "not_advertise"], + "interface": ["name"], "parameters": ["router_id"], } cfg_dict = self.parse_attr(conf, param_lst[param], match) diff --git a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py index 17f63fb2..4bdcbf8c 100644 --- a/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py +++ b/plugins/module_utils/network/vyos/facts/prefix_lists/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py index 2ad54e63..d4084fd7 100644 --- a/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py +++ b/plugins/module_utils/network/vyos/facts/route_maps/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -25,6 +24,13 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.rou from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps import ( Route_mapsTemplate, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.route_maps_14 import ( + Route_mapsTemplate14, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import get_os_version class Route_mapsFacts(object): @@ -59,11 +65,18 @@ class Route_mapsFacts(object): """ facts = {} objs = [] + + if LooseVersion(get_os_version(self._module)) >= LooseVersion("1.4"): + route_maps_class = Route_mapsTemplate14 + else: + route_maps_class = Route_mapsTemplate + if not data: data = self.get_config(connection) # parse native config using the Route_maps template - route_maps_parser = Route_mapsTemplate(lines=data.splitlines()) + route_maps_parser = route_maps_class(lines=data.splitlines()) + if route_maps_parser.parse().get("route_maps"): objs = list(route_maps_parser.parse().get("route_maps").values()) for item in objs: diff --git a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py index e70a15f9..d3ff02e5 100644 --- a/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py +++ b/plugins/module_utils/network/vyos/facts/snmp_server/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ based on the configuration. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.snmp_server.snmp_server import ( @@ -63,7 +61,7 @@ class Snmp_serverFacts(object): if objs: if "communities" in objs: for k in objs["communities"].values(): - for param, val in iteritems(k): + for param, val in k.items(): if param in ["clients", "networks"]: if None in val: val.remove(None) diff --git a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py index 99b3917b..1bce772c 100644 --- a/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py +++ b/plugins/module_utils/network/vyos/facts/static_routes/static_routes.py @@ -12,7 +12,6 @@ based on the configuration. from __future__ import absolute_import, division, print_function - __metaclass__ = type from copy import deepcopy from re import M, findall, search @@ -164,9 +163,9 @@ class Static_routesFacts(object): elif dis >= 1: nh_info["enabled"] = False for element in nh_list: - if element["forward_router_address"] == nh_info["forward_router_address"]: - if "interface" in nh_info.keys(): - element["interface"] = nh_info["interface"] + if element.get("forward_router_address") == nh_info.get( + "forward_router_address", + ): if "admin_distance" in nh_info.keys(): element["admin_distance"] = nh_info["admin_distance"] if "enabled" in nh_info.keys(): @@ -174,4 +173,5 @@ class Static_routesFacts(object): nh_info = None if nh_info is not None: nh_list.append(nh_info) + nh_info = {} return nh_list diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py new file mode 100644 index 00000000..695b9b8d --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos vpn_ipsec fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. + +Follows the established per-key conversion convention used by +vyos_logging_global/vyos_ha (explicit process_facts() naming each +name-keyed dict that needs converting to a list), matching the config.py +convention for this module, rather than a generic argspec-driven walker. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( + Vpn_ipsecTemplate, +) + + +class Vpn_ipsecFacts(object): + """The vyos vpn_ipsec facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vpn_ipsecArgs.argument_spec + + def get_vpn_ipsec_data(self, connection): + return connection.get('show configuration commands | match "vpn ipsec"') + + def process_facts(self, objFinal): + """Convert the name-keyed dicts produced by the parser into the + lists the argspec expects. Each key handled explicitly, matching + the vyos_logging_global/vyos_ha convention. + """ + if not objFinal: + return objFinal + + for key in ("ike_group", "esp_group"): + if key in objFinal: + items = list(objFinal[key].values()) + for item in items: + if "proposal" in item: + item["proposal"] = sorted( + item["proposal"].values(), + key=lambda p: int(p["proposal_id"]), + ) + objFinal[key] = sorted(items, key=lambda item: item["name"]) + + if "profile" in objFinal: + objFinal["profile"] = sorted( + objFinal["profile"].values(), + key=lambda item: item["name"], + ) + + if "authentication" in objFinal: + auth = objFinal["authentication"] + for key in ("psk", "ppk"): + if key in auth: + auth[key] = sorted( + auth[key].values(), + key=lambda item: item["name"], + ) + + return objFinal + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vpn_ipsec network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_vpn_ipsec_data(connection) + + vpn_ipsec_parser = Vpn_ipsecTemplate(lines=data.splitlines(), module=self._module) + objs = vpn_ipsec_parser.parse() + + ansible_facts["ansible_network_resources"].pop("vpn_ipsec", None) + objs = self.process_facts(objs) + + params = utils.remove_empties( + vpn_ipsec_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vpn_ipsec"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..6c9b5d8e --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,115 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos vpn_ipsec_s2s fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. + +Follows the established per-key conversion convention used by +vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts() +naming each name-keyed dict that needs converting to a list), matching +the config.py convention for this module, rather than a generic +argspec-driven walker. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2sFacts(object): + """The vyos vpn_ipsec_s2s facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec + + def get_vpn_ipsec_s2s_data(self, connection): + return connection.get( + 'show configuration commands | match "vpn ipsec site-to-site"', + ) + + def process_facts(self, objFinal): + """Convert the name-keyed dicts produced by the parser into the + lists the argspec expects. + + NOTE: every PARSERS result template in rm_templates.py nests its + output under "site_to_site" -> "peer" (mirroring the CLI's own + tree: `vpn ipsec site-to-site peer <name> ...`), but the + argspec's `config` has `peer` directly at the top level -- there + is no `site_to_site` wrapper in the argspec, since that's the + one node wrap_docstring.py unwrapped when building the + docstring (its own children became config's children directly). + So this needs to strip that outer key, not just convert the + name-keyed dicts to lists. + """ + if not objFinal: + return objFinal + + site_to_site = objFinal.get("site_to_site", {}) + peers = site_to_site.get("peer", {}) + + items = list(peers.values()) + for item in items: + if "tunnel" in item: + item["tunnel"] = sorted( + item["tunnel"].values(), + key=lambda t: int(t["tunnel_id"]), + ) + + return {"peer": sorted(items, key=lambda item: item["name"])} + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vpn_ipsec_s2s network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_vpn_ipsec_s2s_data(connection) + + vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = vpn_ipsec_s2s_parser.parse() + + ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None) + objs = self.process_facts(objs) + + params = utils.remove_empties( + vpn_ipsec_s2s_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vpn_ipsec_s2s"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/vyos/facts/vrf/__init__.py b/plugins/module_utils/network/vyos/facts/vrf/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vrf/__init__.py diff --git a/plugins/module_utils/network/vyos/facts/vrf/vrf.py b/plugins/module_utils/network/vyos/facts/vrf/vrf.py new file mode 100644 index 00000000..74f46b45 --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vrf/vrf.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +""" +The vyos vrf fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vrf.vrf import VrfArgs +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_global.bgp_global import ( + Bgp_globalFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv2.ospfv2 import ( + Ospfv2Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv3.ospfv3 import ( + Ospfv3Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( + Static_routesFacts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vrf import ( + VrfTemplate, +) + + +class VrfFacts(object): + """The vyos vrf facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = VrfArgs.argument_spec + + def get_config(self, connection): + return connection.get("show configuration commands | match 'set vrf'") + + def get_config_set(self, data, connection): + """To classify the configurations beased on vrf""" + config_dict = {} + for config_line in data.splitlines(): + vrf_inst = re.search(r"set vrf name (\S+).*", config_line) + vrf_bta = re.search(r"set vrf bind-to-all", config_line) + if vrf_bta: + config_dict["bind_to_all"] = config_dict.get("bind_to_all", "") + config_line + "\n" + if vrf_inst: + config_dict[vrf_inst.group(1)] = ( + config_dict.get(vrf_inst.group(1), "") + config_line + "\n" + ) + return list(config_dict.values()) + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vrf network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + objs = [] + + if not data: + data = self.get_config(connection) + + vrf_facts = {} + instances = [] + vrf_parser = VrfTemplate(lines=[], module=self._module) + resources = self.get_config_set(data, connection) + + for resource in resources: + vrf_parser = VrfTemplate( + lines=resource.split("\n"), + module=self._module, + ) + objs = vrf_parser.parse() + + if objs and "protocols" in resource: + + protocol_lines = [] + for line in resource.strip().split("\n"): + if "protocols" in line: + idx = line.index("protocols") + protocol_lines.append("set " + line[idx:]) + objs["protocols"] = self._parse_protocols("\n".join(protocol_lines)) + + if objs: + if "bind_to_all" in objs: + vrf_facts.update(objs) + if "name" in objs: + instances.append(self._normalise_instance(objs)) + + if instances: + vrf_facts.update({"instances": instances}) + + ansible_facts["ansible_network_resources"].pop("vrf_facts", None) + facts = {"vrf": []} + + params = utils.remove_empties( + vrf_parser.validate_config( + self.argument_spec, + {"config": vrf_facts}, + redact=True, + ), + ) + + if not resources: + params["config"].pop("bind_to_all", None) + + if params.get("config"): + facts["vrf"] = params["config"] + ansible_facts["ansible_network_resources"].update(facts) + return ansible_facts + + def _normalise_instance(self, instance): + n_inst = instance.copy() + af_map = {} + + for af in instance.get("address_family", []): + afi = af.get("afi") + if not afi: + continue + + if afi not in af_map: + af_map[afi] = {"afi": afi} + + for k, v in af.items(): + if k == "afi": + continue + elif k == "route_maps": + if "route_maps" not in af_map[afi]: + af_map[afi]["route_maps"] = [] + af_map[afi]["route_maps"].extend(v) + else: + af_map[afi][k] = v + + for afi_data in af_map.values(): + if "route_maps" in afi_data: + seen = [] + deduped = [] + for item in afi_data["route_maps"]: + if item not in seen: + seen.append(item) + deduped.append(item) + afi_data["route_maps"] = deduped + + n_inst["address_family"] = list(af_map.values()) + return n_inst + + def _parse_protocols(self, protocols): + """Parse protocols and return a dictionary""" + + protocol_chunks = {} + parsed_protocols = {} + + for line in protocols.split("\n"): + parts = line.split() + if len(parts) > 2 and parts[0] == "set" and parts[1] == "protocols": + protocol = parts[2] + protocol_chunks.setdefault(protocol, []).append(line) + + protocol_strings = {proto: "\n".join(lines) for proto, lines in protocol_chunks.items()} + + for protocol_name, protocol_string in protocol_strings.items(): + protocol_dict = {} + + if protocol_name == "bgp": + bgp_module = Bgp_globalFacts(self._module) + protocol_dict = bgp_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "ospf": + ospf_module = Ospfv2Facts(self._module) + protocol_dict = ospf_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "ospfv3": + ospfv3_module = Ospfv3Facts(self._module) + protocol_dict = ospfv3_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + + elif protocol_name == "static": + static_routes_module = Static_routesFacts(self._module) + protocol_dict = static_routes_module.populate_facts( + connection=self._module._connection, + ansible_facts={"ansible_network_resources": {}}, + data=protocol_string, + ) + parsed_protocols[protocol_name] = list( + protocol_dict.get("ansible_network_resources").values(), + )[0] + else: + self._module.fail_json(msg="The protocol is not supported" + protocol_name) + + return parsed_protocols diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py index f8f86cd2..fe7889aa 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -293,8 +292,8 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+aggregate-address \s+(?P<address>\S+) - \s*(?P<as_set>as-set)* - \s*(?P<summary_only>summary-only)* + \s*(?P<as_set>as-set)? + \s*(?P<summary_only>summary-only)? $""", re.VERBOSE, ), @@ -397,8 +396,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+path-limit - \s+(?P<limit>\S+) - *$""", + \s+(?P<limit>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -432,8 +430,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -499,8 +496,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+metric - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -534,8 +530,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -568,8 +563,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+table - \s+(?P<tab>\S+) - *$""", + \s+(?P<tab>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -659,8 +653,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+allowas-in \s+number - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -877,8 +870,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+capability \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -915,8 +907,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -992,8 +983,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1033,8 +1023,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1073,8 +1062,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1176,8 +1164,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1246,8 +1233,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1389,8 +1375,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1424,8 +1409,7 @@ class Bgp_address_familyTemplate(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py index fd4c9de9..9936cf6d 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_address_family_14.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -309,8 +308,8 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+aggregate-address \s+(?P<address>\S+) - \s*(?P<as_set>as-set)* - \s*(?P<summary_only>summary-only)* + \s*(?P<as_set>as-set)? + \s*(?P<summary_only>summary-only)? $""", re.VERBOSE, ), @@ -410,8 +409,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+path-limit - \s+(?P<limit>\S+) - *$""", + \s+(?P<limit>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -444,8 +442,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+network \s+(?P<address>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_network, @@ -508,8 +505,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+(?P<proto>\S+) - \s+metric\s+(?P<val>\S+) - *$""", + \s+metric\s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -542,8 +538,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+redistribute \s+(?P<proto>\S+) \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -575,8 +570,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+redistribute \s+table - \s+(?P<tab>\S+) - *$""", + \s+(?P<tab>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_redistribute, @@ -663,8 +657,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+allowas-in \s+number - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -875,8 +868,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+capability \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -912,8 +904,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -987,8 +978,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_prefix_list, @@ -1027,8 +1017,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_filter_list, @@ -1066,8 +1055,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1166,8 +1154,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1234,8 +1221,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+(?P<afi>\S+)-unicast \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor_route_map, @@ -1373,8 +1359,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, @@ -1407,8 +1392,7 @@ class Bgp_address_familyTemplate14(NetworkTemplate): \s+address-family \s+(?P<afi>\S+)-unicast \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_af_neighbor, diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py index 621f65ea..730fa5ee 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ the given network resource. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( NetworkTemplate, ) @@ -26,7 +24,7 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r def _tmplt_bgp_params_confederation(config_data): command = [] for list_el in config_data["bgp_params"]["confederation"]: - for k, v in iteritems(list_el): + for k, v in list_el.items(): command.append( "protocols bgp {as_number} parameters confederation ".format(**config_data) + k @@ -65,7 +63,7 @@ def _tmplt_bgp_params_default(config_data): def _tmplt_bgp_neighbor_timers(config_data): command = [] - for k, v in iteritems(config_data["neighbor"]["timers"]): + for k, v in config_data["neighbor"]["timers"].items(): command.append( "protocols bgp {as_number} neighbor ".format(**config_data) + config_data["neighbor"]["address"] @@ -80,7 +78,7 @@ def _tmplt_bgp_neighbor_timers(config_data): def _tmplt_bgp_timers(config_data): command = [] - for k, v in iteritems(config_data["timers"]): + for k, v in config_data["timers"].items(): command.append( "protocols bgp {as_number} ".format(**config_data) + "timers " + k + " " + str(v), ) @@ -238,8 +236,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+advertisement-interval - \s+(?P<interval>\S+) - *$""", + \s+(?P<interval>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -438,8 +435,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -468,8 +464,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -495,8 +490,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+description - \s+(?P<desc>\S+) - *$""", + \s+(?P<desc>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -574,8 +568,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+disable-send-community - \s+(?P<comm>\S+) - *$""", + \s+(?P<comm>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -634,8 +627,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ebgp-multihop - \s+(?P<hop>\S+) - *$""", + \s+(?P<hop>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -662,8 +654,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -722,8 +713,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -827,8 +817,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+password - \s+(?P<pwd>\S+) - *$""", + \s+(?P<pwd>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -854,8 +843,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -881,8 +869,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+port - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -909,8 +896,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -941,8 +927,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+remote-as - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -995,8 +980,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1158,8 +1142,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1185,8 +1168,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+update-source - \s+(?P<src>\S+) - *$""", + \s+(?P<src>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1212,8 +1194,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1239,8 +1220,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ttl-security - \s+(?P<ttl>\S+) - *$""", + \s+(?P<ttl>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1267,8 +1247,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<address>\S+) \s+timers \s+(?P<type>connect|holdtime|keepalive) - \s+(?P<sec>\S+) - *$""", + \s+(?P<sec>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1296,8 +1275,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+timers \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1419,8 +1397,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+cluster-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1443,8 +1420,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+confederation \s+(?P<type>identifier|peers) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1472,8 +1448,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1498,8 +1473,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1524,8 +1498,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1550,8 +1523,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1575,9 +1547,8 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+default - \s*(?P<no_ipv4_unicast>no-ipv4-unicast)* - \s*(?P<local_pref>local-pref\s\S+) - *$""", + \s*(?P<no_ipv4_unicast>no-ipv4-unicast)? + \s*(?P<local_pref>local-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1649,8 +1620,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+distance\sprefix \s+(?P<prefix>\S+) \s+distance - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1679,8 +1649,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1730,8 +1699,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1819,8 +1787,7 @@ class Bgp_globalTemplate(NetworkTemplate): \s+(?P<as_num>\d+) \s+parameters \s+router-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp {{ as_number }} parameters router-id {{ bgp_params.router_id }}", diff --git a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py index b8beb923..b86e233c 100644 --- a/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/bgp_global_14.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -17,7 +16,6 @@ the given network resource. import re -from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( NetworkTemplate, ) @@ -26,12 +24,9 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r def _tmplt_bgp_params_confederation(config_data): command = [] for list_el in config_data["bgp_params"]["confederation"]: - for k, v in iteritems(list_el): + for k, v in list_el.items(): command.append( - "protocols bgp parameters confederation ".format(**config_data) - + k - + " " - + str(v), + "protocols bgp parameters confederation ".format(**config_data) + k + " " + str(v), ) return command @@ -65,7 +60,7 @@ def _tmplt_bgp_params_default(config_data): def _tmplt_bgp_neighbor_timers(config_data): command = [] - for k, v in iteritems(config_data["neighbor"]["timers"]): + for k, v in config_data["neighbor"]["timers"].items(): command.append( "protocols bgp neighbor ".format(**config_data) + config_data["neighbor"]["address"] @@ -80,7 +75,7 @@ def _tmplt_bgp_neighbor_timers(config_data): def _tmplt_bgp_timers(config_data): command = [] - for k, v in iteritems(config_data["timers"]): + for k, v in config_data["timers"].items(): command.append( "protocols bgp ".format(**config_data) + "timers " + k + " " + str(v), ) @@ -238,8 +233,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+advertisement-interval - \s+(?P<interval>\S+) - *$""", + \s+(?P<interval>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} advertisement-interval {{ neighbor.advertisement_interval }}", @@ -431,8 +425,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+capability \s+orf \s+prefix-list - \s+(?P<orf>\S+) - *$""", + \s+(?P<orf>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} capability orf prefix-list {{ neighbor.capability.orf }}", @@ -460,8 +453,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+default-originate \s+route-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} default-originate route-map {{ neighbor.default_originate }}", @@ -486,8 +478,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+description - \s+(?P<desc>\S+) - *$""", + \s+(?P<desc>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} description {{ neighbor.description }}", @@ -562,8 +553,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+disable-send-community - \s+(?P<comm>\S+) - *$""", + \s+(?P<comm>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} disable-send-community {{ neighbor.disable_send_community }}", @@ -620,8 +610,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ebgp-multihop - \s+(?P<hop>\S+) - *$""", + \s+(?P<hop>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ebgp-multihop {{ neighbor.ebgp_multihop }}", @@ -647,8 +636,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+filter-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_filter_list, @@ -705,8 +693,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+maximum-prefix - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} maximum-prefix {{ neighbor.maximum_prefix }}", @@ -806,8 +793,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+password - \s+(?P<pwd>\S+) - *$""", + \s+(?P<pwd>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} password {{ neighbor.password }}", @@ -832,8 +818,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+peer-group - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} peer-group {{ neighbor.peer_group_name }}", @@ -858,8 +843,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+port - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} port {{ neighbor.port }}", @@ -885,8 +869,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+prefix-list \s+(?P<action>export|import) - \s+(?P<list>\S+) - *$""", + \s+(?P<list>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_prefix_list, @@ -916,8 +899,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+remote-as - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as }}", @@ -968,8 +950,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+route-map \s+(?P<action>export|import) - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_route_map, @@ -1125,8 +1106,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+unsuppress-map - \s+(?P<map>\S+) - *$""", + \s+(?P<map>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} unsuppress-map {{ neighbor.unsuppress_map }}", @@ -1151,8 +1131,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+update-source - \s+(?P<src>\S+) - *$""", + \s+(?P<src>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} update-source {{ neighbor.update_source }}", @@ -1177,8 +1156,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+weight - \s+(?P<num>\S+) - *$""", + \s+(?P<num>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} weight {{ neighbor.weight }}", @@ -1203,8 +1181,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+neighbor \s+(?P<address>\S+) \s+ttl-security - \s+(?P<ttl>\S+) - *$""", + \s+(?P<ttl>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp neighbor {{ neighbor.address }} ttl-security {{ neighbor.ttl_security }}", @@ -1230,8 +1207,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+(?P<address>\S+) \s+timers \s+(?P<type>connect|holdtime|keepalive) - \s+(?P<sec>\S+) - *$""", + \s+(?P<sec>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_neighbor_timers, @@ -1258,8 +1234,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+timers \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_timers, @@ -1376,8 +1351,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+cluster-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters cluster-id {{ bgp_params.cluster_id }}", @@ -1399,8 +1373,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+confederation \s+(?P<type>identifier|peers) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_confederation, @@ -1427,8 +1400,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+half-life - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening half-life {{ bgp_params.dampening.half_life}}", @@ -1452,8 +1424,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+max-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening max-suppress-time {{ bgp_params.dampening.max_suppress_time}}", @@ -1477,8 +1448,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+re-use - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening re-use {{ bgp_params.dampening.re_use}}", @@ -1502,8 +1472,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+dampening \s+start-suppress-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters dampening start-suppress-time {{ bgp_params.dampening.start_suppress_time}}", @@ -1526,9 +1495,8 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+default - \s*(?P<no_ipv4_unicast>no-ipv4-unicast)* - \s*(?P<local_pref>local-pref\s\S+) - *$""", + \s*(?P<no_ipv4_unicast>no-ipv4-unicast)? + \s*(?P<local_pref>local-pref\s\S+)?\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_default, @@ -1597,8 +1565,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+distance\sprefix \s+(?P<prefix>\S+) \s+distance - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters distance prefix {{ bgp_params.distance.prefix }} distance {{ bgp_params.distance.value }}", @@ -1626,8 +1593,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+parameters \s+distance\sglobal \s+(?P<type>\S+) - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_bgp_params_distance, @@ -1675,8 +1641,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+graceful-restart\s+stalepath-time - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters graceful-restart stalepath-time {{ bgp_params.graceful_restart }}", @@ -1760,8 +1725,7 @@ class Bgp_globalTemplate14(NetworkTemplate): \s+bgp \s+parameters \s+router-id - \s+(?P<id>\S+) - *$""", + \s+(?P<id>\S+)\s*$""", re.VERBOSE, ), "setval": "protocols bgp parameters router-id {{ bgp_params.router_id }}", diff --git a/plugins/module_utils/network/vyos/rm_templates/ha.py b/plugins/module_utils/network/vyos/rm_templates/ha.py new file mode 100644 index 00000000..300b14a7 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/ha.py @@ -0,0 +1,1011 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The Ha parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_vsrvs(config_data): + config_data = config_data["virtual_servers"] + command = [] + + cmd = "high-availability virtual-server {name}".format(**config_data) + for key, value in config_data.items(): + if key == "name" or isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vsrvs_rsrv(config_data): + config_data = config_data["virtual_servers"] + command = [] + cmd = "high-availability virtual-server {name}".format(**config_data) + config_data = config_data["real_server"] + address = config_data["address"] + for key, value in config_data.items(): + if key == "address" or value is None: + continue + if value is not None and key == "health_check_script": + command.append(cmd + " real-server " + address + " health-check script " + value) + else: + command.append(cmd + " real-server " + f"{address} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_hc(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_ts(config_data): + config_data = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp(config_data): + config_data = config_data["vrrp"]["global_parameters"] + command = [] + + cmd = "high-availability vrrp global-parameters".format(**config_data) + for key, value in config_data.items(): + if isinstance(value, dict) or value is None: + continue + else: + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_gp_garp(config_data): + config_data = config_data["vrrp"]["global_parameters"]["garp"] + command = [] + cmd = "high-availability vrrp global-parameters garp" + + for key, value in config_data.items(): + if value is None: + continue + command.append(f"{cmd} {key.replace('_', '-')} {value}") + + return command + + +def _tmplt_vrrp_group(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if ( + key == "name" + or isinstance(value, dict) + or isinstance(value, list) + or isinstance(value, bool) + or value is None + ): + continue + else: + if key == "description": + value = f"'{value}'" + command.append(f"{cmd} {key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(f"{cmd} {key.replace('_', '-')}") + return command + + +def _tmplt_vrrp_group_garp(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["garp"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " garp " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_auth(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["authentication"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " authentication " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_ts(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["transition_script"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " transition-script " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_sgroup_member(config_data): + sgroup = config_data["vrrp"]["sync_groups"] + command = [] + cmd = "high-availability vrrp sync-group {name}".format(**sgroup) + members = sgroup.get("member", []) + for member in members: + if member is None: + continue + command.append(f"{cmd} member {member}") + return command + + +def _tmplt_vrrp_group_exaddress(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + exaddresses = group.get("excluded_address", []) + for exaddress in exaddresses: + if exaddress is None: + continue + command.append(f"{cmd} excluded-address {exaddress}") + return command + + +def _tmplt_vrrp_group_address(config_data): + group = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**group) + addresses = group.get("address", []) + for address in addresses: + if address is None: + continue + command.append(f"{cmd} address {address}") + return command + + +def _tmplt_vrrp_group_hc(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["health_check"] + for key, value in config_data.items(): + if value is not None: + command.append(cmd + " health-check " + f"{key.replace('_', '-')} {value}") + return command + + +def _tmplt_vrrp_group_track_list(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if isinstance(value, list) and value is not None and key != "name": + for item in value: + command.append(cmd + " track " + f"{key.replace('_', '-')} {item}") + return command + + +def _tmplt_vrrp_group_track_bool(config_data): + config_data = config_data["vrrp"]["groups"] + command = [] + cmd = "high-availability vrrp group {name}".format(**config_data) + config_data = config_data["track"] + for key, value in config_data.items(): + if key != "name" and value is not None: + command.append(cmd + " track " + f"{key.replace('_', '-')}") + return command + + +class HaTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(HaTemplate, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "disable", + "getval": re.compile( + r""" + ^set + \shigh-availability + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": "high-availability disable", + "result": { + "disable": "{{ True if disable is defined else False }}", + }, + }, + { + "name": "virtual_servers.address", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+address\s+(?P<address>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "address": "{{ address if address is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.algorithm", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+algorithm\s+(?P<algorithm>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "algorithm": "{{ algorithm if algorithm is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.delay_loop", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+delay-loop\s+(?P<delay_loop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "delay_loop": "{{ delay_loop if delay_loop is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.forward_method", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+forward-method\s+(?P<forward_method>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "forward_method": "{{ forward_method if forward_method is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.fwmark", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+fwmark\s+(?P<fwmark>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "fwmark": "{{ fwmark if fwmark is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.persistence_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+persistence-timeout\s+(?P<persistence_timeout>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "persistence_timeout": "{{ persistence_timeout if persistence_timeout is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+port\s+(?P<port>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.protocol", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + (?:\s+protocol\s+(?P<protocol>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "protocol": "{{ protocol if protocol is defined else None }}", + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.port", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+port\s+(?P<port>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "port": "{{ port if port is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.health_check_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+health-check\sscript\s+(?P<hcscript>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "health_check_script": "{{ hcscript if hcscript is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "virtual_servers.real_server.connection_timeout", + "getval": re.compile( + r""" + ^set\shigh-availability\svirtual-server + \s+(?P<name>\S+) + \sreal-server + \s+(?P<address>\S+) + (?:\s+connection-timeout\s+(?P<cont>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vsrvs_rsrv, + "result": { + "virtual_servers": { + "{{ name }}": { + "name": "{{ name }}", + "real_server": { + "{{ address }}": { + "address": "{{ address }}", + "connection_timeout": "{{ cont if cont is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.member", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \smember + \s+(?P<member>\S+) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_member, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "member": [ + "{{ member }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.health_check", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \shealth-check + (?:\s+failure-count\s+(?P<failure_count>\S+))? + (?:\s+interval\s+(?P<int>\S+))? + (?:\s+ping\s+(?P<ping>\S+))? + (?:\s+script\s+(?P<script>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_hc, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "health_check": { + "failure_count": "{{ failure_count if failure_count is defined else None }}", + "interval": "{{ int if int is defined else None }}", + "ping": "{{ ping if ping is defined else None }}", + "script": "{{ script if script is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.sync_groups.transition_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\ssync-group + \s+(?P<sgname>\S+) + \stransition-script + (?:\s+backup\s+(?P<backup>\S+))? + (?:\s+fault\s+(?P<fault>\S+))? + (?:\s+master\s+(?P<master>\S+))? + (?:\s+stop\s+(?P<stop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_sgroup_ts, + "result": { + "vrrp": { + "sync_groups": { + "{{ sgname }}": { + "name": "{{ sgname }}", + "transition_script": { + "backup": "{{ backup if backup is defined else None }}", + "fault": "{{ fault if fault is defined else None }}", + "master": "{{ master if master is defined else None }}", + "stop": "{{ stop if stop is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.global_parameters.garp", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sglobal-parameters + \s+garp + (?:\s+interval\s+(?P<interval>\S+))? + (?:\s+master-delay\s+(?P<master_delay>\S+))? + (?:\s+master-refresh\s+(?P<master_refresh>\S+))? + (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))? + (?:\s+master-repeat\s+(?P<master_repeat>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_gp_garp, + "result": { + "vrrp": { + "global_parameters": { + "garp": { + "interval": "{{ interval if interval is defined else None }}", + "master_delay": "{{ master_delay if master_delay is defined else None }}", + "master_refresh": "{{ master_refresh if master_refresh is defined else None }}", + "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}", + "master_repeat": "{{ master_repeat if master_repeat is defined else None }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.global_parameters", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sglobal-parameters + (?=\s+(?:startup-delay|version)\s) + (?:\s+startup-delay\s+(?P<startup_delay>\S+))? + (?:\s+version\s+(?P<version>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_gp, + "result": { + "vrrp": { + "global_parameters": { + "startup_delay": "{{ startup_delay if startup_delay is defined else None }}", + "version": "{{ version if version is defined else None }}", + }, + }, + }, + }, + { + "name": "vrrp.groups", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + (?:\s+description\s+(?P<description>'.+?'|\S+))? + (?:\s+advertise-interval\s+(?P<advertise_interval>\S+))? + (?:\s+hello-source-address\s+(?P<hello_source>\S+))? + (?:\s+interface\s+(?P<interface>\S+))? + (?:\s+peer-address\s+(?P<peer_address>\S+))? + (?:\s+preempt-delay\s+(?P<preempt_delay>\S+))? + (?:\s+priority\s+(?P<priority>\S+))? + (?:\s+vrid\s+(?P<vrid>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "description": "{{ description | replace(\"'\", \"\") if description is defined else None }}", + "advertise_interval": "{{ advertise_interval if advertise_interval is defined else None }}", + "hello_source_address": "{{ hello_source if hello_source is defined else None }}", + "interface": "{{ interface if interface is defined else None }}", + "peer_address": "{{ peer_address if peer_address is defined else None }}", + "preempt_delay": "{{ preempt_delay if preempt_delay is defined else None }}", + "priority": "{{ priority if priority is defined else None }}", + "vrid": "{{ vrid if vrid is defined else None }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.excluded_address", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \sexcluded-address + \s+(?P<excluded_address>.*) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_exaddress, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "excluded_address": [ + "{{ excluded_address | replace(\"'\", \"\") }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.address", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \saddress + \s+(?P<address>.*) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_address, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "address": [ + "{{ address | replace(\"'\", \"\") }}", + ], + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.garp", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \s+garp + (?:\s+interval\s+(?P<interval>\S+))? + (?:\s+master-delay\s+(?P<master_delay>\S+))? + (?:\s+master-refresh\s+(?P<master_refresh>\S+))? + (?:\s+master-refresh-repeat\s+(?P<master_refresh_repeat>\S+))? + (?:\s+master-repeat\s+(?P<master_repeat>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_garp, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "garp": { + "interval": "{{ interval if interval is defined else None }}", + "master_delay": "{{ master_delay if master_delay is defined else None }}", + "master_refresh": "{{ master_refresh if master_refresh is defined else None }}", + "master_refresh_repeat": "{{ master_refresh_repeat if master_refresh_repeat is defined else None }}", + "master_repeat": "{{ master_repeat if master_repeat is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.authentication", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \s+authentication + (?:\s+password\s+(?P<password>\S+))? + (?:\s+type\s+(?P<type>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_auth, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "authentication": { + "password": "{{ password if password is defined else None }}", + "type": "{{ type if type is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.transition_script", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \stransition-script + (?:\s+backup\s+(?P<backup>\S+))? + (?:\s+fault\s+(?P<fault>\S+))? + (?:\s+master\s+(?P<master>\S+))? + (?:\s+stop\s+(?P<stop>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_ts, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "transition_script": { + "backup": "{{ backup if backup is defined else None }}", + "fault": "{{ fault if fault is defined else None }}", + "master": "{{ master if master is defined else None }}", + "stop": "{{ stop if stop is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.health_check", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \shealth-check + (?:\s+failure-count\s+(?P<failure_count>\S+))? + (?:\s+interval\s+(?P<int>\S+))? + (?:\s+ping\s+(?P<ping>\S+))? + (?:\s+script\s+(?P<script>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_hc, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "health_check": { + "failure_count": "{{ failure_count if failure_count is defined else None }}", + "interval": "{{ int if int is defined else None }}", + "ping": "{{ ping if ping is defined else None }}", + "script": "{{ script if script is defined else None }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.track.interface", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \strack + (?:\s+interface\s+(?P<interface>\S+))? + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_track_list, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "track": { + "interface": "{{ [interface.strip(\"'\")] if interface is defined else [] }}", + }, + }, + }, + }, + }, + }, + { + "name": "vrrp.snmp", + "getval": re.compile( + r""" + ^set + \shigh-availability + \svrrp + \s(?P<snmp>snmp) + $""", + re.VERBOSE, + ), + "setval": "high-availability vrrp snmp", + "result": { + "vrrp": { + "snmp": "{{ 'enabled' if snmp is defined else 'disabled' }}", + }, + }, + }, + { + "name": "vrrp.groups.disable", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "disable": "{{ True if disable is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.no_preempt", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<no_preempt>no-preempt) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "no_preempt": "{{ True if no_preempt is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.rfc3768_compatibility", + "getval": re.compile( + r""" + ^set + \shigh-availability\svrrp\sgroup + \s(?P<gname>\S+) + \s(?P<rfc3768_compatibility>rfc3768-compatibility) + $""", + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "rfc3768_compatibility": "{{ True if rfc3768_compatibility is defined else False }}", + }, + }, + }, + }, + }, + { + "name": "vrrp.groups.track.exclude_vrrp_interface", + "getval": re.compile( + r""" + ^set\shigh-availability\svrrp\sgroup + \s+(?P<gname>\S+) + \strack + \s(?P<exclude_vrrp_inter>exclude-vrrp-interface) + $ + """, + re.VERBOSE, + ), + "setval": _tmplt_vrrp_group_track_bool, + "result": { + "vrrp": { + "groups": { + "{{ gname }}": { + "name": "{{ gname }}", + "track": { + "exclude_vrrp_interface": "{{ True if exclude_vrrp_inter is defined else False }}", + }, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/hostname.py b/plugins/module_utils/network/vyos/rm_templates/hostname.py index 29ab00f6..b7e56310 100644 --- a/plugins/module_utils/network/vyos/rm_templates/hostname.py +++ b/plugins/module_utils/network/vyos/rm_templates/hostname.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global.py b/plugins/module_utils/network/vyos/rm_templates/logging_global.py index 516e270b..07ad1f68 100644 --- a/plugins/module_utils/network/vyos/rm_templates/logging_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/logging_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py new file mode 100644 index 00000000..3216747d --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/logging_global_15.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def tmplt_params(config_data): + def templt_common(val, tmplt): + if val.get("facility"): + tmplt += " facility {facility}".format(facility=val["facility"]) + if val.get("severity"): + tmplt += " level {level}".format(level=val["severity"]) + return tmplt + + tmplt = "" + if config_data.get("global_params"): + val = config_data.get("global_params") + tmplt += "system syslog local" + tmplt = templt_common(val.get("facilities", {}), tmplt) + elif config_data.get("console"): + val = config_data.get("console") + tmplt += "system syslog console" + tmplt = templt_common(val.get("facilities", {}), tmplt) + elif config_data.get("hosts"): + val = config_data.get("hosts") + if val.get("hostname") and not val.get("port") and not val.get("protocol"): + tmplt += "system syslog remote {hostname}".format(hostname=val["hostname"]) + if val.get("facilities"): + tmplt = templt_common(val.get("facilities"), tmplt) + return tmplt + + +class Logging_globalTemplate15(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Logging_globalTemplate15, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + { + "name": "syslog.state", + "getval": re.compile( + r""" + ^set\ssystem + (\s(?P<syslog>syslog)) + $""", re.VERBOSE, + ), + "setval": "system syslog", + "result": { + "syslog": { + "state": "{{ 'enabled' if syslog is defined else 'disabled' }}", + }, + }, + }, + { + "name": "console.state", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<console>console)) + $""", re.VERBOSE, + ), + "setval": "system syslog console", + "result": { + "console": { + "state": "{{ 'enabled' if console is defined else 'disabled' }}", + }, + }, + }, + { + "name": "console.facilities", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sconsole\sfacility + (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))? + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog console facility {{ console.facilities.facility }}", + "result": { + "console": { + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + { + "name": "global_params.state", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<local>local)) + $""", re.VERBOSE, + ), + "setval": "system syslog local", + "result": { + "global_params": { + "state": "{{ 'enabled' if local is defined else 'disabled' }}", + }, + }, + }, + { + "name": "global_params.marker_interval", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\smarker\sinterval + (\s(?P<marker_interval>'(\d+)'))? + $""", re.VERBOSE, + ), + "setval": "system syslog marker interval {{ global_params.marker_interval }}", + "remval": "system syslog marker", + "result": { + "global_params": { + "marker_interval": "{{ marker_interval }}", + }, + }, + }, + { + "name": "global_params.preserve_fqdn", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog + (\s(?P<preserve_fqdn>preserve-fqdn)) + $""", re.VERBOSE, + ), + "setval": "system syslog preserve-fqdn", + "result": { + "global_params": { + "preserve_fqdn": "{{ True if preserve_fqdn is defined }}", + }, + }, + }, + { + "name": "global_params.facilities", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\slocal\sfacility + (\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7]))? + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog local facility {{ global_params.facilities.facility }}", + "result": { + "global_params": { + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + { + "name": "hosts.port", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sport\s(?P<port>'(\d+)')) + $""", re.VERBOSE, + ), + "setval": "system syslog remote {{ hosts.hostname }} port {{ hosts.port }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "port": "{{ port }}", + }, + }, + }, + }, + { + "name": "hosts.protocol", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sprotocol\s(?P<protocol>'(udp|tcp)')) + $""", re.VERBOSE, + ), + "setval": "system syslog remote {{ hosts.hostname }} protocol {{ hosts.protocol }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "protocol": "{{ protocol }}", + }, + }, + }, + }, + { + "name": "hosts", + "getval": re.compile( + r""" + ^set\ssystem\ssyslog\sremote + (\s(?P<hostname>\S+)) + (\sfacility\s(?P<facility>all|auth|authpriv|cron|daemon|kern|lpr|mail|mark|news|protocols|security|syslog|user|uucp|local[0-7])) + (\slevel\s(?P<level>'(emerg|alert|crit|err|warning|notice|info|debug|all)'))? + $""", re.VERBOSE, + ), + "setval": tmplt_params, + "remval": "system syslog remote {{ hosts.hostname }}", + "result": { + "hosts": { + "{{ hostname }}": { + "hostname": "{{ hostname }}", + "facilities": [ + { + "facility": "{{ facility }}", + "severity": "{{ level }}", + }, + ], + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/nat.py b/plugins/module_utils/network/vyos/rm_templates/nat.py new file mode 100644 index 00000000..94af8696 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/nat.py @@ -0,0 +1,1186 @@ +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +def _tmplt_nat_rule_addr_sub(config_data): + """Generate address/fqdn/prefix/port/group commands for destination or source sub-dict.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + atype = config_data["atype"] + sub = config_data["sub"] + + base = f"{nat} {type_} rule {rid} {atype}" + commands = [] + + for field in ("address", "fqdn", "prefix", "port"): + if sub.get(field) is not None: + commands.append(f"{base} {field} {sub[field]}") + + for gtype in ("address_group", "domain_group", "mac_group", "network_group", "port_group"): + if sub.get(gtype) is not None: + commands.append(f"{base} group {gtype.replace('_', '-')} {sub[gtype]}") + + return commands + + +def _tmplt_nat_rule_translation(config_data): + """Generate translation commands.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + trans = config_data["translation"] + + base = f"{nat} {type_} rule {rid} translation" + commands = [] + + if trans.get("address") is not None: + commands.append(f"{base} address {trans['address']}") + + if trans.get("port") is not None: + commands.append(f"{base} port {trans['port']}") + + if trans.get("redirect_port") is not None: + commands.append(f"{base} redirect port {trans['redirect_port']}") + + if trans.get("address_mapping") is not None: + commands.append(f"{base} options address-mapping {trans['address_mapping']}") + + if trans.get("port_mapping") is not None: + commands.append(f"{base} options port-mapping {trans['port_mapping']}") + + return commands + + +def _tmplt_nat64_translation_pool(config_data): + """Generate all nat64 translation pool commands from a single call.""" + nat = config_data["nat"] + type_ = config_data["type"] + rid = config_data["id"] + pool_id = config_data["pool_id"] + pool = config_data["pool"] + + base = f"{nat} {type_} rule {rid} translation pool {pool_id}" + commands = [] + + if pool.get("address") is not None: + commands.append(f"{base} address {pool['address']}") + if pool.get("description") is not None: + commands.append(f"{base} description '{pool['description']}'") + if pool.get("port") is not None: + commands.append(f"{base} port {pool['port']}") + if pool.get("protocol") is not None: + commands.append(f"{base} protocol {pool['protocol']}") + if pool.get("disable"): + commands.append(f"{base} disable") + + return commands + + +class NatTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(NatTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module) + + # fmt: off + PARSERS = [ + + # ------------------------- + # CGNAT + # ------------------------- + { + "name": "cgnat_log_allocation", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+log-allocation + $""", + re.VERBOSE, + ), + "setval": "nat cgnat log-allocation", + "result": { + "nat": { + "cgnat": { + "log_allocation": True, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+range + \s+(?P<range>\S+)(?:\s+seq\s+(?P<seq>\d+))? + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} range {{ range }}{% if seq is defined and seq %} seq {{ seq }}{% endif %}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "range": [ + { + "value": "{{ range }}", + "seq": "{{ seq }}", + }, + ], + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_port_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+external-port-range + \s+(?P<range>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} external-port-range {{ range }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "external_port_range": "{{ range }}", + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_external_per_user", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+external + \s+(?P<name>\S+) + \s+per-user-limit + \s+port + \s+(?P<limit>\d+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool external {{ name }} per-user-limit port {{ limit }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "external": [ + { + "name": "{{ name }}", + "per_user_limit": {"port": "{{ limit }}"}, + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_pool_internal_range", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+pool + \s+internal + \s+(?P<name>\S+) + \s+range + \s+(?P<range>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat pool internal {{ name }} range {{ range }}", + "result": { + "nat": { + "cgnat": { + "pool": { + "internal": [ + { + "name": "{{ name }}", + "range": ["{{ range }}"], + }, + ], + }, + }, + }, + }, + }, + { + "name": "cgnat_rule_source_pool", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+rule + \s+(?P<id>\d+) + \s+source + \s+pool + \s+(?P<pool>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat rule {{ id }} source pool {{ pool }}", + "result": { + "nat": { + "cgnat": { + "rule": [ + { + "id": "{{ id }}", + "source": {"pool": "{{ pool }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "cgnat_rule_translation_pool", + "getval": re.compile( + r""" + ^set + \s+nat + \s+cgnat + \s+rule + \s+(?P<id>\d+) + \s+translation + \s+pool + \s+(?P<pool>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat cgnat rule {{ id }} translation pool {{ pool }}", + "result": { + "nat": { + "cgnat": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"pool": "{{ pool }}"}, + }, + ], + }, + }, + }, + }, + + # ------------------------- + # GENERIC NAT + # ------------------------- + { + "name": "nat_type_description", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+description + \s+(?P<description>.+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} description '{{ description }}'", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "description": "{{ description }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_protocol", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+protocol + \s+(?P<protocol>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} protocol {{ protocol }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "protocol": "{{ protocol }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_disable", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+disable + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} disable", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "disable": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_exclude", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+exclude + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} exclude", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "exclude": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_log", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+log + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} log", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "log": True, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_prefix", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+prefix + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"prefix": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_fqdn", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+fqdn + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"fqdn": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_address_group", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+(?P<atype>destination|source) + \s+group + \s+(?P<gtype>address-group|domain-group|mac-group|network-group|port-group) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_addr_sub, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "{{ atype }}": { + "{{ gtype | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_address", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"address": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_port", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": {"port": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_options", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+options + \s+(?P<opt>address-mapping|port-mapping) + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "{{ opt | replace('-', '_') }}": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_translation_redirect", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+redirect + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": _tmplt_nat_rule_translation, + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "redirect_port": "{{ value }}", + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_inbound_interface_name", + "getval": re.compile( + r""" + ^set + \s+nat + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat {{ type }} rule {{ id }} inbound-interface name {{ value }}", + "result": { + "nat": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_inbound_interface_group", + "getval": re.compile( + r""" + ^set + \s+nat + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+group + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat {{ type }} rule {{ id }} inbound-interface group {{ value }}", + "result": { + "nat": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"group": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_static_inbound_interface", + "getval": re.compile( + r""" + ^set + \s+nat + \s+static + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat static rule {{ id }} inbound-interface {{ value }}", + "result": { + "nat": { + "static": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat6x_inbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+inbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} inbound-interface name {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "inbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_outbound_interface", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+name + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface name {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "outbound_interface": {"name": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_outbound_interface_group", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source|static) + \s+rule + \s+(?P<id>\S+) + \s+outbound-interface + \s+group + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} outbound-interface group {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "outbound_interface": {"group": "{{ value }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_packet_type", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+packet-type + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} packet-type {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "packet_type": "{{ value }}", + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_backend", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+backend + \s+(?P<ip>\S+) + \s+weight + \s+(?P<weight>\d+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance backend {{ ip }} weight {{ weight }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "load_balance": { + "backend": [ + {"ip": "{{ ip }}", "weight": "{{ weight }}"}, + ], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat_type_lb_hash", + "getval": re.compile( + r""" + ^set + \s+(?P<nat>nat|nat64|nat66) + \s+(?P<type>destination|source) + \s+rule + \s+(?P<id>\S+) + \s+load-balance + \s+hash + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "{{ nat }} {{ type }} rule {{ id }} load-balance hash {{ value }}", + "result": { + "{{ nat }}": { + "{{ type }}": { + "rule": [ + { + "id": "{{ id }}", + "load_balance": {"hash": ["{{ value }}"]}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_match_mark", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+match + \s+mark + \s+(?P<mark>\d+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} match mark {{ mark }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "match": {"mark": "{{ mark }}"}, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool", + "getval": re.compile(r"^$"), # never matches — setval only + "setval": _tmplt_nat64_translation_pool, + "result": {}, + }, + { + "name": "nat64_translation_pool_address", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+address + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} address {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "address": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + + { + "name": "nat64_translation_pool_description", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+description + \s+(?P<value>.+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} description '{{ value }}'", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "description": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_disable", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+disable + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} disable", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "disable": True}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_port", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+port + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} port {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "port": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + { + "name": "nat64_translation_pool_protocol", + "getval": re.compile( + r""" + ^set + \s+nat64 + \s+source + \s+rule + \s+(?P<id>\S+) + \s+translation + \s+pool + \s+(?P<pool_id>\d+) + \s+protocol + \s+(?P<value>\S+) + $""", + re.VERBOSE, + ), + "setval": "nat64 source rule {{ id }} translation pool {{ pool_id }} protocol {{ value }}", + "result": { + "nat64": { + "source": { + "rule": [ + { + "id": "{{ id }}", + "translation": { + "pool": [{"id": "{{ pool_id }}", "protocol": "{{ value }}"}], + }, + }, + ], + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py index e9d8a0cb..b83a3e1c 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ntp_global.py +++ b/plugins/module_utils/network/vyos/rm_templates/ntp_global.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py index 0d7eaf84..5183aec4 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -23,6 +22,7 @@ from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.r from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( get_interface_type, + get_interface_with_vif, ) @@ -36,21 +36,23 @@ def _get_parameters(data): def _tmplt_ospf_int_delete(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) - command = ( - "interfaces " + int_type + " {name} ".format(**config_data) + params[1] + " " + params[0] - ) + command = "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] return command def _tmplt_ospf_int_cost(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -62,11 +64,14 @@ def _tmplt_ospf_int_cost(config_data): def _tmplt_ospf_int_auth_password(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -79,11 +84,14 @@ def _tmplt_ospf_int_auth_password(config_data): def _tmplt_ospf_int_auth_md5(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -98,11 +106,14 @@ def _tmplt_ospf_int_auth_md5(config_data): def _tmplt_ospf_int_auth_md5_delete(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -114,11 +125,14 @@ def _tmplt_ospf_int_auth_md5_delete(config_data): def _tmplt_ospf_int_bw(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -130,11 +144,14 @@ def _tmplt_ospf_int_bw(config_data): def _tmplt_ospf_int_hello_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -146,11 +163,14 @@ def _tmplt_ospf_int_hello_interval(config_data): def _tmplt_ospf_int_dead_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -162,15 +182,10 @@ def _tmplt_ospf_int_dead_interval(config_data): def _tmplt_ospf_int_mtu_ignore(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( - "interfaces " - + int_type - + " {name} ".format(**config_data) - + params[1] - + " " - + params[0] - + " mtu-ignore" + "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] + " mtu-ignore" ) return command @@ -178,11 +193,14 @@ def _tmplt_ospf_int_mtu_ignore(config_data): def _tmplt_ospf_int_network(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -194,11 +212,14 @@ def _tmplt_ospf_int_network(config_data): def _tmplt_ospf_int_priority(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -210,11 +231,14 @@ def _tmplt_ospf_int_priority(config_data): def _tmplt_ospf_int_retransmit_interval(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -226,11 +250,14 @@ def _tmplt_ospf_int_retransmit_interval(config_data): def _tmplt_ospf_int_transmit_delay(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -242,11 +269,14 @@ def _tmplt_ospf_int_transmit_delay(config_data): def _tmplt_ospf_int_ifmtu(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -258,11 +288,14 @@ def _tmplt_ospf_int_ifmtu(config_data): def _tmplt_ospf_int_instance(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) command = ( "interfaces " + int_type - + " {name} ".format(**config_data) + + " " + + name + + " " + params[1] + " " + params[0] @@ -274,16 +307,9 @@ def _tmplt_ospf_int_instance(config_data): def _tmplt_ospf_int_passive(config_data): int_type = get_interface_type(config_data["name"]) + name = get_interface_with_vif(config_data["name"]) params = _get_parameters(config_data["address_family"]) - command = ( - "interfaces " - + int_type - + " {name} ".format(**config_data) - + params[1] - + " " - + params[0] - + " passive" - ) + command = "interfaces " + int_type + " " + name + " " + params[1] + " " + params[0] + " passive" return command @@ -308,6 +334,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) *$""", @@ -316,7 +343,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "remval": _tmplt_ospf_int_delete, "compval": "address_family", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -332,18 +359,18 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+authentication \s+plaintext-password - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, "compval": "address_family.authentication", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -362,6 +389,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+authentication @@ -369,15 +397,14 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+key-id \s+(?P<id>\d+) \s+md5-key - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, "remval": _tmplt_ospf_int_auth_md5_delete, "compval": "address_family.authentication", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -399,6 +426,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+bandwidth @@ -409,7 +437,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_bw, "compval": "address_family.bandwidth", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -426,6 +454,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+cost @@ -436,7 +465,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_cost, "compval": "address_family.cost", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -453,6 +482,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+hello-interval @@ -463,7 +493,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_hello_interval, "compval": "address_family.hello_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -480,6 +510,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+dead-interval @@ -490,7 +521,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_dead_interval, "compval": "address_family.dead_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -507,6 +538,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+(?P<mtu>mtu-ignore) @@ -516,7 +548,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_mtu_ignore, "compval": "address_family.mtu_ignore", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -533,17 +565,17 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+network - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, "compval": "address_family.network", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -560,6 +592,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+priority @@ -570,7 +603,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_priority, "compval": "address_family.priority", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -587,6 +620,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+retransmit-interval @@ -597,7 +631,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_retransmit_interval, "compval": "address_family.retransmit_interval", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -614,6 +648,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+transmit-delay @@ -624,7 +659,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_transmit_delay, "compval": "address_family.transmit_delay", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -641,6 +676,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+ifmtu @@ -651,7 +687,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_ifmtu, "compval": "address_family.ifmtu", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -668,6 +704,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+instance-id @@ -678,7 +715,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_instance, "compval": "address_family.instance", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -695,6 +732,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? \s+(?P<afi>ip|ipv6) \s+(?P<proto>ospf|ospfv3) \s+(?P<pass>passive) @@ -704,7 +742,7 @@ class Ospf_interfacesTemplate(NetworkTemplate): "setval": _tmplt_ospf_int_passive, "compval": "address_family.passive", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", "address_family": { "{{ afi }}": { "afi": '{{ "ipv4" if afi == "ip" else "ipv6" }}', @@ -721,12 +759,13 @@ class Ospf_interfacesTemplate(NetworkTemplate): \s+interfaces \s+(?P<type>\S+) \s+(?P<name>\S+) + (?:\s+vif\s+(?P<vif>\d+))? .*$""", re.VERBOSE, ), "setval": "set interface {{ type }} {{ name }}", "result": { - "name": "{{ name }}", + "name": "{{ name + '.' + vif if vif is defined else name }}", }, }, ] diff --git a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py index 43fae1e9..8d09011f 100644 --- a/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py +++ b/plugins/module_utils/network/vyos/rm_templates/ospf_interfaces_14.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type """ @@ -31,9 +32,7 @@ def _get_parameters(data): def _tmplt_ospf_int_delete(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " + params[0] + " interface {name}".format(**config_data) - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) return command @@ -81,10 +80,7 @@ def _tmplt_ospf_int_auth_md5(config_data): def _tmplt_ospf_int_auth_md5_delete(config_data): params = _get_parameters(config_data["address_family"]) command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " authentication" + "protocols " + params[0] + " interface {name}".format(**config_data) + " authentication" ) return command @@ -128,12 +124,7 @@ def _tmplt_ospf_int_dead_interval(config_data): def _tmplt_ospf_int_mtu_ignore(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " mtu-ignore" - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) + " mtu-ignore" return command @@ -212,12 +203,7 @@ def _tmplt_ospf_int_instance(config_data): def _tmplt_ospf_int_passive(config_data): params = _get_parameters(config_data["address_family"]) - command = ( - "protocols " - + params[0] - + " interface {name}".format(**config_data) - + " passive" - ) + command = "protocols " + params[0] + " interface {name}".format(**config_data) + " passive" return command @@ -239,8 +225,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+protocols \s+(?P<proto>ospf|ospfv3) \s+interface - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "remval": _tmplt_ospf_int_delete, @@ -265,8 +250,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+(?P<name>\S+) \s+authentication \s+plaintext-password - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_password, @@ -297,8 +281,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+key-id \s+(?P<id>\d+) \s+md5-key - \s+(?P<text>\S+) - *$""", + \s+(?P<text>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_auth_md5, @@ -458,8 +441,7 @@ class Ospf_interfacesTemplate14(NetworkTemplate): \s+interface \s+(?P<name>\S+) \s+network - \s+(?P<val>\S+) - *$""", + \s+(?P<val>\S+)\s*$""", re.VERBOSE, ), "setval": _tmplt_ospf_int_network, diff --git a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py index 0e99cfea..0e071199 100644 --- a/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py +++ b/plugins/module_utils/network/vyos/rm_templates/prefix_lists.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps.py b/plugins/module_utils/network/vyos/rm_templates/route_maps.py index 8f218a6b..fd8fdd9d 100644 --- a/plugins/module_utils/network/vyos/rm_templates/route_maps.py +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -33,8 +32,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "route_map", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$""", re.VERBOSE, ), "compval": "route_map", @@ -51,8 +49,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\s*$""", re.VERBOSE, ), "compval": "sequence", @@ -75,8 +72,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "call", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", @@ -99,8 +95,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "description", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", @@ -123,8 +118,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "action", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", @@ -147,8 +141,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "continue_sequence", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+)\s*$""", re.VERBOSE, ), "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", @@ -171,8 +164,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_next", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next)\s*$""", re.VERBOSE, ), "compval": "on_match.next", @@ -198,8 +190,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "on_match_goto", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+)\s*$""", re.VERBOSE, ), "compval": "on_match.goto", @@ -225,8 +216,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_ip", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.ip", @@ -254,8 +244,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_aggregator_as", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+)\s*$""", re.VERBOSE, ), "compval": "set.aggregator.as", @@ -283,8 +272,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_as_path_exclude", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-exclude\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-exclude\s(?P<as>\S+)\s*$""", re.VERBOSE, ), "compval": "set.as_path_exclude", @@ -310,12 +298,12 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_as_path_prepend", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-prepend\s(?P<as>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path-prepend\s(?P<as>.*) + $""", re.VERBOSE, ), "compval": "set.as_path_prepend", - "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path-prepend {{set.as_path_prepend}}", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path-prepend '{{set.as_path_prepend}}'", "result": { "route_maps": { "{{ route_map }}": { @@ -337,10 +325,10 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_atomic_aggregate", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\satomic-aggregate(?P<as>) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\s(?P<as>atomic-aggregate)\s*$""", re.VERBOSE, ), + "compval": "set.atomic_aggregate", "setval": "policy route-map {{route_map}} rule {{sequence}} set atomic-aggregate", "result": { "route_maps": { @@ -363,8 +351,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_bgp_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+)\s*$""", re.VERBOSE, ), "compval": "set.bgp_extcommunity_rt", @@ -391,13 +378,12 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\scomm-list\s(?P<comm_list>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<comm_list>\S+)\s*$""", re.VERBOSE, ), - "compval": "set.comm_list.comm_list", + "compval": "match.community.community_list", "setval": "policy route-map {{route_map}} rule {{sequence}} " - "set comm-list comm-list {{set.comm_list.comm_list}}", + "match community community-list {{set.comm_list.comm_list}}", "result": { "route_maps": { "{{ route_map }}": { @@ -406,8 +392,8 @@ class Route_mapsTemplate(NetworkTemplate): "{{sequence}}": { "sequence": "{{sequence}}", - "set": { - "comm_list": {"comm_list": "{{comm_list}}"}, + "match": { + "community": {"community_list": "{{comm_list}}"}, }, }, }, @@ -419,8 +405,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_comm_list_delete", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\sdelete(?P<delete>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\s(?P<delete>delete)\s*$""", re.VERBOSE, ), "compval": "set.comm_list.comm_list", @@ -447,8 +432,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_rt", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-rt\s(?P<extcommunity_rt>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-rt\s(?P<extcommunity_rt>\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_rt", @@ -475,8 +459,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_extcommunity_soo", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-soo\s(?P<extcommunity_soo>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity-soo\s(?P<extcommunity_soo>\S+)\s*$""", re.VERBOSE, ), "compval": "set.extcommunity_soo", @@ -500,11 +483,65 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "set_extcommunity_bandwidth", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\sbandwidth\s(?P<extcommunity_bw>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth {{set.extcommunity_bandwidth}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth": "{{extcommunity_bw}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth_non_transitive", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) + \sset\sextcommunity\s(?P<extcommunity_bw_nt>bandwidth-non-transitive)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth_non_transitive", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth-non-transitive", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth_non_transitive": "{{True if extcommunity_bw_nt is defined}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "set_ip_next_hop", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+)\s*$""", re.VERBOSE, ), "compval": "set.ip_next_hop", @@ -533,8 +570,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sipv6-next-hop \s(?P<type>global|local) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "set.ipv6_next_hop", @@ -564,8 +600,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_large_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<large_community>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<large_community>\S+)\s*$""", re.VERBOSE, ), "compval": "set.large_community", @@ -592,8 +627,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_local_preference", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+)\s*$""", re.VERBOSE, ), "compval": "set.local_preference", @@ -620,8 +654,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric", @@ -648,8 +681,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_metric_type", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+)\s*$""", re.VERBOSE, ), "compval": "set.metric_type", @@ -676,8 +708,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+)\s*$""", re.VERBOSE, ), "compval": "set.origin", @@ -704,8 +735,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_originator_id", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+)\s*$""", re.VERBOSE, ), "compval": "set.originator_id", @@ -732,8 +762,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_src", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+)\s*$""", re.VERBOSE, ), "compval": "set.src", @@ -760,8 +789,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_tag", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+)\s*$""", re.VERBOSE, ), "compval": "set.tag", @@ -788,8 +816,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "set_weight", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+)\s*$""", re.VERBOSE, ), "compval": "set.weight", @@ -813,11 +840,37 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "set_table", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stable\s(?P<table>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set table {{set.table}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "table": "{{table}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "set_community", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<value>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "set.community.value", @@ -846,8 +899,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_as_path", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+)\s*$""", re.VERBOSE, ), "compval": "match.as_path", @@ -874,8 +926,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+)\s*$""", re.VERBOSE, ), "compval": "match.community.community_list", @@ -902,8 +953,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_community_exact_match", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>)\s*$""", re.VERBOSE, ), "compval": "match.community.exact_match", @@ -930,8 +980,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_extcommunity", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+)\s*$""", re.VERBOSE, ), "compval": "match.extcommunity", @@ -958,8 +1007,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_interface", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+)\s*$""", re.VERBOSE, ), "compval": "match.interface", @@ -986,8 +1034,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_large_community_large_community_list", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+)\s*$""", re.VERBOSE, ), "compval": "match.large_community_large_community_list", @@ -1014,8 +1061,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_metric", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+)\s*$""", re.VERBOSE, ), "compval": "match.metric", @@ -1042,8 +1088,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_origin", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+)\s*$""", re.VERBOSE, ), "compval": "match.origin", @@ -1070,8 +1115,7 @@ class Route_mapsTemplate(NetworkTemplate): "name": "match_peer", "getval": re.compile( r""" - ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+) - *$""", + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+)\s*$""", re.VERBOSE, ), "compval": "match.peer", @@ -1100,8 +1144,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\saddress \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.address", @@ -1134,8 +1177,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\snexthop \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.next_hop", @@ -1168,8 +1210,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\sroute-source \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ip.route_source", @@ -1202,8 +1243,7 @@ class Route_mapsTemplate(NetworkTemplate): r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\saddress \s(?P<list_type>access-list|prefix-list) - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.address", @@ -1235,8 +1275,7 @@ class Route_mapsTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\snexthop - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.ipv6.next_hop", @@ -1261,12 +1300,37 @@ class Route_mapsTemplate(NetworkTemplate): }, }, { + "name": "match_protocol", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sprotocol\s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.protocol", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match protocol {{match.protocol}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "protocol": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + { "name": "match_rpki", "getval": re.compile( r""" ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\srpki - \s(?P<value>\S+) - *$""", + \s(?P<value>\S+)\s*$""", re.VERBOSE, ), "compval": "match.rpki", diff --git a/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py new file mode 100644 index 00000000..cf2d6b67 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/route_maps_14.py @@ -0,0 +1,1363 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +""" +The Route_maps parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Route_mapsTemplate14(NetworkTemplate): + def __init__(self, lines=None): + prefix = {"set": "set", "remove": "delete"} + super(Route_mapsTemplate14, self).__init__(lines=lines, tmplt=self, prefix=prefix) + + # fmt: off + PARSERS = [ + { + "name": "route_map", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "route_map", + "setval": "policy route-map {{route_map}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + }, + }, + }, + }, + { + "name": "sequence", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+) + *$""", + re.VERBOSE, + ), + "compval": "sequence", + "setval": "policy route-map {{route_map}} rule {{sequence}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + }, + }, + }, + }, + }, + }, + { + "name": "call", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scall\s(?P<call>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} call {{call}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "call": "{{call}}", + }, + }, + }, + }, + }, + }, + { + "name": "description", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sdescription\s(?P<description>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} description {{description}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "description": "{{description}}", + }, + }, + }, + }, + }, + }, + { + "name": "action", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\saction\s(?P<action>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} action {{action}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "action": "{{action}}", + }, + }, + }, + }, + }, + }, + { + "name": "continue_sequence", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\scontinue\s(?P<continue>\S+)\s*$""", + re.VERBOSE, + ), + "setval": "policy route-map {{route_map}} rule {{sequence}} continue {{continue_sequence}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "continue_sequence": "{{continue}}", + }, + }, + }, + }, + }, + }, + { + "name": "on_match_next", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\s(?P<next>next) + *$""", + re.VERBOSE, + ), + "compval": "on_match.next", + "setval": "policy route-map {{route_map}} rule {{sequence}} on-match next", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "on_match": { + "next": "{{True if next is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "on_match_goto", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\son-match\sgoto\s(?P<goto>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "on_match.goto", + "setval": "policy route-map {{route_map}} rule {{sequence}} on-match goto {{on_match.goto}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "on_match": { + "goto": "{{goto}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_aggregator_ip", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sip\s(?P<ip>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.aggregator.ip", + "setval": "policy route-map {{route_map}} rule {{sequence}} set aggregator ip {{set.aggregator.ip}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "aggregator": { + "ip": "{{ip}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_aggregator_as", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\saggregator\sas\s(?P<as>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.aggregator.as", + "setval": "policy route-map {{route_map}} rule {{sequence}} set aggregator as {{set.aggregator.as}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "aggregator": { + "as": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_as_path_exclude", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path\sexclude\s(?P<as>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.as_path_exclude", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path exclude {{set.as_path_exclude}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "as_path_exclude": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_as_path_prepend", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sas-path\sprepend\s(?P<as>.*) + $""", + re.VERBOSE, + ), + "compval": "set.as_path_prepend", + "setval": "policy route-map {{route_map}} rule {{sequence}} set as-path prepend '{{set.as_path_prepend}}'", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "as_path_prepend": "{{as}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_atomic_aggregate", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\s(?P<as>atomic-aggregate) + *$""", + re.VERBOSE, + ), + "compval": "set.atomic_aggregate", + "setval": "policy route-map {{route_map}} rule {{sequence}} set atomic-aggregate", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "atomic_aggregate": "{{True if as is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_bgp_extcommunity_rt", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sbgp-extcommunity-rt\s(?P<bgp>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.bgp_extcommunity_rt", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set bgp-extcommunity-rt {{set.bgp_extcommunity_rt}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "bgp_extcommunity_rt": "{{bgp}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_comm_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<comm_list>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.community.community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community community-list {{set.comm_list.comm_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"community_list": "{{comm_list}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_comm_list_delete", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scomm-list\s(?P<delete>delete)\s*$""", + re.VERBOSE, + ), + "compval": "set.comm_list.comm_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set comm-list delete", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "comm_list": {"delete": "{{True if delete is defined}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_rt", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\srt\s(?P<extcommunity_rt>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_rt", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity rt {{set.extcommunity_rt}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_rt": "{{extcommunity_rt}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_soo", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\ssoo\s(?P<extcommunity_soo>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_soo", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity soo {{set.extcommunity_soo}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_soo": "{{extcommunity_soo}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\sbandwidth\s(?P<extcommunity_bw>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth {{set.extcommunity_bandwidth}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth": "{{extcommunity_bw}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_extcommunity_bandwidth_non_transitive", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sextcommunity\s(?P<extcommunity_bw_nt>bandwidth-non-transitive) + *$""", + re.VERBOSE, + ), + "compval": "set.extcommunity_bandwidth_non_transitive", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set extcommunity bandwidth-non-transitive", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "extcommunity_bandwidth_non_transitive": "{{True if extcommunity_bw_nt is defined}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_ip_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sip-next-hop\s(?P<ip_next_hop>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.ip_next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set ip-next-hop {{set.ip_next_hop}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "ip_next_hop": "{{ip_next_hop}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_ipv6_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sipv6-next-hop + \s(?P<type>global|local) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.ipv6_next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set ipv6-next-hop {{set.ipv6_next_hop.ip_type}} {{set.ipv6_next_hop.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "ipv6_next_hop": { + "ip_type": "{{type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_large_community", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slarge-community\s(?P<op>none|replace\s(?P<large_community>\S+)) + $""", + re.VERBOSE, + ), + "compval": "set.large_community", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set large-community {{set.large_community if set.large_community == 'none' else 'replace ' + set.large_community}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "large_community": "{{op if op == 'none' else large_community}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_local_preference", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\slocal-preference\s(?P<local_preference>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.local_preference", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set local-preference {{set.local_preference}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "local_preference": "{{local_preference}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_metric", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric\s(?P<metric>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.metric", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set metric {{set.metric}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "metric": "{{metric}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_metric_type", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\smetric-type\s(?P<metric_type>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.metric_type", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set metric-type {{set.metric_type}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "metric_type": "{{metric_type}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_origin", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sorigin\s(?P<origin>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.origin", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set origin {{set.origin}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "origin": "{{origin}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_originator_id", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\soriginator-id\s(?P<originator_id>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.originator_id", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set originator-id {{set.originator_id}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "originator_id": "{{originator_id}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_src", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\ssrc\s(?P<src>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.src", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set src {{set.src}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "src": "{{src}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_tag", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stag\s(?P<tag>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.tag", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set tag {{set.tag}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "tag": "{{tag}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_weight", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\sweight\s(?P<weight>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set weight {{set.weight}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "weight": "{{weight}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_table", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\stable\s(?P<table>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "set.weight", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set table {{set.table}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "table": "{{table}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "set_community", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\sset\scommunity\s(?P<op>none|replace\s(?P<value>\S+)) + $""", + re.VERBOSE, + ), + "compval": "set.community.value", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "set community {{set.community.value if set.community.value == 'none' else 'replace ' + set.community.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "set": { + "community": { + "value": "{{op if op == 'none' else value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_as_path", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sas-path\s(?P<as_path>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.as_path", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match as-path {{match.as_path}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "as_path": "{{as_path}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_community_community_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\scommunity-list\s(?P<community_list>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.community.community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community community-list {{match.community.community_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"community_list": "{{community_list}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_community_exact_match", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\scommunity\sexact-match(?P<exact_match>) + *$""", + re.VERBOSE, + ), + "compval": "match.community.exact_match", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match community exact-match", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "community": {"exact_match": "{{True if exact_match is defined}}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_extcommunity", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sextcommunity\s(?P<extcommunity>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.extcommunity", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match extcommunity {{match.extcommunity}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "extcommunity": "{{extcommunity}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_interface", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sinterface\s(?P<interface>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.interface", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match interface {{match.interface}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "interface": "{{interface}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_large_community_large_community_list", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\slarge-community\slarge-community-list\s(?P<lc>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.large_community_large_community_list", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match large-community large-community-list {{match.large_community_large_community_list}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "large_community_large_community_list": "{{lc}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_metric", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\smetric\s(?P<metric>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.metric", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match metric {{match.metric}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "metric": "{{metric}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_origin", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sorigin\s(?P<origin>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.origin", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match origin {{match.origin}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "origin": "{{origin}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_peer", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\speer\s(?P<peer>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.peer", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match peer {{match.peer}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": + { + "sequence": "{{sequence}}", + "match": { + "peer": "{{peer}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_address", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\saddress + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.address", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip address {{match.ip.address.list_type}} {{match.ip.address.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "address": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_next_hop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\snexthop + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip nexthop {{match.ip.next_hop.list_type}} {{match.ip.next_hop.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "next_hop": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ip_route_source", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sip\sroute-source + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ip.route_source", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ip route-source {{match.ip.route_source.list_type}} {{match.ip.route_source.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ip": { + "route_source": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ipv6_address", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\saddress + \s(?P<list_type>access-list|prefix-list) + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ipv6.address", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ipv6 address {{match.ipv6.address.list_type}} {{match.ipv6.address.value}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ipv6": { + "address": { + "list_type": "{{list_type}}", + "value": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_ipv6_nexthop", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sipv6\snexthop + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.ipv6.next_hop", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match ipv6 nexthop {{match.ipv6.next_hop}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "ipv6": { + "next_hop": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_protocol", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\sprotocol\s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.protocol", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match protocol {{match.protocol}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "protocol": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "match_rpki", + "getval": re.compile( + r""" + ^set\spolicy\sroute-map\s(?P<route_map>\S+)\srule\s(?P<sequence>\d+)\smatch\srpki + \s(?P<value>\S+)\s*$""", + re.VERBOSE, + ), + "compval": "match.rpki", + "setval": "policy route-map {{route_map}} rule {{sequence}} " + "match rpki {{match.rpki}}", + "result": { + "route_maps": { + "{{ route_map }}": { + "route_map": '{{ route_map }}', + "entries": { + "{{sequence}}": { + "sequence": "{{sequence}}", + "match": { + "rpki": "{{value}}", + }, + }, + }, + }, + }, + }, + }, + + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py index 71753083..bd76a5ae 100644 --- a/plugins/module_utils/network/vyos/rm_templates/snmp_server.py +++ b/plugins/module_utils/network/vyos/rm_templates/snmp_server.py @@ -5,7 +5,6 @@ from __future__ import absolute_import, division, print_function - __metaclass__ = type """ @@ -140,9 +139,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\scommunity \s+(?P<name>\S+) - \s*(?P<auth>authorization\srw|authorization\sro)* - \s*(client\s(?P<client>\S+))* - \s*(network\s(?P<network>\S+))* + \s*(?P<auth>authorization\srw|authorization\sro)? + \s*(client\s(?P<client>\S+))? + \s*(network\s(?P<network>\S+))? $""", re.VERBOSE, ), @@ -164,8 +163,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\scontact - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp contact {{ contact }}", @@ -179,8 +177,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sdescription - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp description {{ description }}", @@ -195,8 +192,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\slisten-address \s+(?P<addr>\S+) - \s*(port)* - \s*(?P<port>\d+)* + \s*(port)? + \s*(?P<port>\d+)? $""", re.VERBOSE, ), @@ -232,8 +229,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\ssmux-peer - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp smux-peer {{ smux_peer }}", @@ -247,8 +243,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\strap-source - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp trap-source {{ trap_source }}", @@ -263,9 +258,8 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\strap-target \s+(?P<name>\S+) - \s*(?P<comm>community\s\S+)* - \s*(?P<port>port\s\d+)* - $""", + \s*(?P<comm>community\s\S+)? + \s*(?P<port>port\s\d+)? $""", re.VERBOSE, ), "setval": _tmplt_snmp_server_trap_target, @@ -283,8 +277,7 @@ class Snmp_serverTemplate(NetworkTemplate): "getval": re.compile( r""" ^set\sservice\ssnmp\sv3\sengineid - \s+(?P<name>\S+) - *$""", + \s+(?P<name>\S+)\s*$""", re.VERBOSE, ), "setval": "service snmp v3 engineid {{ snmp_v3.engine_id }}", @@ -301,9 +294,9 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\sgroup \s+(?P<name>\S+) - \s*(?P<mode>mode\s\S+)* - \s*(?P<sec>seclevel\s\S+)* - \s*(?P<view>view\s\S+)* + \s*(?P<mode>mode\s\S+)? + \s*(?P<sec>seclevel\s\S+)? + \s*(?P<view>view\s\S+)? $""", re.VERBOSE, ), @@ -329,9 +322,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) \s+auth - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -358,8 +351,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<port>port\s\d+)* - $""", + \s+(?P<port>port\s\d+)? $""", re.VERBOSE, ), "setval": "service snmp v3 trap-target port {{ snmp_v3.trap_targets.port }}", @@ -381,7 +373,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<protocol>protocol\s\S+)* + \s+(?P<protocol>protocol\s\S+)? $""", re.VERBOSE, ), @@ -404,7 +396,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<type>type\s\S+)* + \s+(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -427,7 +419,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) - \s+(?P<user>user\s\S+)* + \s+(?P<user>user\s\S+)? $""", re.VERBOSE, ), @@ -451,9 +443,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\strap-target \s+(?P<name>\S+) \s+privacy - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -481,9 +473,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) \s+auth - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -511,9 +503,9 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) \s+privacy - \s*(?P<enc>encrypted-password\s\S+)* - \s*(?P<plain>plaintext-password\s\S+)* - \s*(?P<type>type\s\S+)* + \s*(?P<enc>encrypted-password\s\S+)? + \s*(?P<plain>plaintext-password\s\S+)? + \s*(?P<type>type\s\S+)? $""", re.VERBOSE, ), @@ -540,8 +532,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) - \s+(?P<group>group\s.+)* - $""", + \s+(?P<group>group\s.+)? $""", re.VERBOSE, ), "setval": "service snmp v3 user {{ snmp_v3.users.user }} group {{ snmp_v3.users.group }}", @@ -563,7 +554,7 @@ class Snmp_serverTemplate(NetworkTemplate): r""" ^set\sservice\ssnmp\sv3\suser \s+(?P<name>\S+) - \s+(?P<mode>mode\s\S+)* + \s+(?P<mode>mode\s\S+)? $""", re.VERBOSE, ), @@ -587,8 +578,8 @@ class Snmp_serverTemplate(NetworkTemplate): ^set\sservice\ssnmp\sv3\sview \s+(?P<name>\S+) \s+(?P<oid>oid\s\S+) - \s*(?P<ex>exclude\s\S+)* - \s*(?P<mask>mask\s\S+)* + \s*(?P<ex>exclude\s\S+)? + \s*(?P<mask>mask\s\S+)? $""", re.VERBOSE, ), diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py new file mode 100644 index 00000000..ff889526 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py @@ -0,0 +1,997 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The VPN IPSEC parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Vpn_ipsecTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Vpn_ipsecTemplate, self).__init__( + lines=lines, + tmplt=self, + prefix=prefix, + module=module, + ) + + # fmt: off + PARSERS = [ + # --------------------------------------------------------------- + # esp-group + # --------------------------------------------------------------- + { + "name": "esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + }, + }, + }, + }, + { + "name": "esp_group.proposal", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + }, + }, + }, + }, + }, + }, + { + "name": "esp_group.proposal.encryption", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sencryption\s'?(?P<encryption>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "encryption": "{{ encryption }}", + }, + }, + }, + }, + }, + }, + { + "name": "esp_group.proposal.hash", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \shash\s'?(?P<hash>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "hash": "{{ hash }}", + }, + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # ike-group + # --------------------------------------------------------------- + { + "name": "ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + }, + }, + }, + }, + { + "name": "ike_group.key_exchange", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \skey-exchange\s'?(?P<key_exchange>\w+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} key-exchange {{ key_exchange }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "key_exchange": "{{ key_exchange }}", + }, + }, + }, + }, + { + "name": "ike_group.proposal", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.dh_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sdh-group\s'?(?P<dh_group>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} dh-group {{ dh_group }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "dh_group": "{{ dh_group }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.encryption", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \sencryption\s'?(?P<encryption>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "encryption": "{{ encryption }}", + }, + }, + }, + }, + }, + }, + { + "name": "ike_group.proposal.hash", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sproposal\s(?P<proposal_id>\d+) + \shash\s'?(?P<hash>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "proposal": { + "{{ proposal_id }}": { + "proposal_id": "{{ proposal_id }}", + "hash": "{{ hash }}", + }, + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication psk + # --------------------------------------------------------------- + { + "name": "authentication.psk", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + }, + }, + }, + }, + }, + { + "name": "authentication.psk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \sid\s'?(?P<id>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} id {{ id }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "id": ["{{ id }}"], + }, + }, + }, + }, + }, + { + "name": "authentication.psk.secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \ssecret\s'?(?P<secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} secret '{{ secret }}'", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "secret": "{{ secret }}", + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # profile + # --------------------------------------------------------------- + { + "name": "profile", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + }, + }, + }, + }, + { + "name": "profile.authentication.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sauthentication\smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication mode {{ mode }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "authentication": { + "mode": "{{ mode }}", + }, + }, + }, + }, + }, + { + "name": "profile.authentication.pre_shared_secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sauthentication\spre-shared-secret\s'?(?P<pre_shared_secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ pre_shared_secret }}'", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "authentication": { + "pre_shared_secret": "{{ pre_shared_secret }}", + }, + }, + }, + }, + }, + { + "name": "profile.bind_tunnel", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sbind\stunnel\s'?(?P<bind_tunnel>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} bind tunnel {{ bind_tunnel }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "bind_tunnel": ["{{ bind_tunnel }}"], + }, + }, + }, + }, + { + "name": "profile.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sesp-group\s'?(?P<esp_group>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} esp-group {{ esp_group }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "esp_group": "{{ esp_group }}", + }, + }, + }, + }, + { + "name": "profile.ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sike-group\s'?(?P<ike_group>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} ike-group {{ ike_group }}", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "ike_group": "{{ ike_group }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # ike-group: remaining fields + # --------------------------------------------------------------- + { + "name": "ike_group.close_action", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sclose-action\s'?(?P<close_action>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} close-action {{ close_action }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "close_action": "{{ close_action }}", + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.action", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\saction\s'?(?P<action>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection action {{ action }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"action": "{{ action }}"}, + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.interval", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\sinterval\s'?(?P<interval>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection interval {{ interval }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"interval": "{{ interval }}"}, + }, + }, + }, + }, + { + "name": "ike_group.dead_peer_detection.timeout", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdead-peer-detection\stimeout\s'?(?P<timeout>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection timeout {{ timeout }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "dead_peer_detection": {"timeout": "{{ timeout }}"}, + }, + }, + }, + }, + { + "name": "ike_group.disable_mobike", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sdisable-mobike + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} disable-mobike", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "disable_mobike": True, + }, + }, + }, + }, + { + "name": "ike_group.ikev2_reauth", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \sikev2-reauth + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} ikev2-reauth", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "ikev2_reauth": True, + }, + }, + }, + }, + { + "name": "ike_group.lifetime", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \slifetime\s'?(?P<lifetime>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} lifetime {{ lifetime }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "lifetime": "{{ lifetime }}", + }, + }, + }, + }, + { + "name": "ike_group.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+) + \smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec ike-group {{ name }} mode {{ mode }}", + "result": { + "ike_group": { + "{{ ike_group }}": { + "name": "{{ ike_group }}", + "mode": "{{ mode }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # esp-group: remaining fields + # --------------------------------------------------------------- + { + "name": "esp_group.compression", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \scompression + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} compression", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "compression": True, + }, + }, + }, + }, + { + "name": "esp_group.disable_rekey", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \sdisable-rekey + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} disable-rekey", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "disable_rekey": True, + }, + }, + }, + }, + { + "name": "esp_group.life_bytes", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slife-bytes\s'?(?P<life_bytes>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} life-bytes {{ life_bytes }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "life_bytes": "{{ life_bytes }}", + }, + }, + }, + }, + { + "name": "esp_group.life_packets", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slife-packets\s'?(?P<life_packets>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} life-packets {{ life_packets }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "life_packets": "{{ life_packets }}", + }, + }, + }, + }, + { + "name": "esp_group.lifetime", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \slifetime\s'?(?P<lifetime>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} lifetime {{ lifetime }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "lifetime": "{{ lifetime }}", + }, + }, + }, + }, + { + "name": "esp_group.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \smode\s'?(?P<mode>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} mode {{ mode }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "mode": "{{ mode }}", + }, + }, + }, + }, + { + "name": "esp_group.pfs", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+) + \spfs\s'?(?P<pfs>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec esp-group {{ name }} pfs {{ pfs }}", + "result": { + "esp_group": { + "{{ esp_group }}": { + "name": "{{ esp_group }}", + "pfs": "{{ pfs }}", + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication.psk: remaining fields + # --------------------------------------------------------------- + { + "name": "authentication.psk.secret_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \ssecret-type\s'?(?P<secret_type>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} secret-type {{ secret_type }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "secret_type": "{{ secret_type }}", + }, + }, + }, + }, + }, + { + "name": "authentication.psk.dhcp_interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+) + \sdhcp-interface\s'?(?P<dhcp_interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication psk {{ name }} dhcp-interface {{ dhcp_interface }}", + "result": { + "authentication": { + "psk": { + "{{ psk }}": { + "name": "{{ psk }}", + "dhcp_interface": ["{{ dhcp_interface }}"], + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # authentication.ppk + # --------------------------------------------------------------- + { + "name": "authentication.ppk", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \sid\s'?(?P<id>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} id {{ id }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "id": ["{{ id }}"], + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.secret", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \ssecret\s'?(?P<secret>[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} secret '{{ secret }}'", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "secret": "{{ secret }}", + }, + }, + }, + }, + }, + { + "name": "authentication.ppk.secret_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+) + \ssecret-type\s'?(?P<secret_type>[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec authentication ppk {{ name }} secret-type {{ secret_type }}", + "result": { + "authentication": { + "ppk": { + "{{ ppk }}": { + "name": "{{ ppk }}", + "secret_type": "{{ secret_type }}", + }, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # profile: remaining fields + # --------------------------------------------------------------- + { + "name": "profile.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sprofile\s(?P<profile>\S+) + \sdisable + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} disable", + "result": { + "profile": { + "{{ profile }}": { + "name": "{{ profile }}", + "disable": True, + }, + }, + }, + }, + + # --------------------------------------------------------------- + # top-level: interface, log, options, disable_uniqreqids + # --------------------------------------------------------------- + { + "name": "interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sinterface\s'?(?P<interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec interface {{ interface }}", + "result": { + "interface": ["{{ interface }}"], + }, + }, + { + "name": "log.level", + "getval": re.compile( + r""" + ^set\svpn\sipsec\slog\slevel\s'?(?P<level>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec log level {{ level }}", + "result": { + "log": {"level": "{{ level }}"}, + }, + }, + { + "name": "log.subsystem", + "getval": re.compile( + r""" + ^set\svpn\sipsec\slog\ssubsystem\s'?(?P<subsystem>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec log subsystem {{ subsystem }}", + "result": { + "log": {"subsystem": ["{{ subsystem }}"]}, + }, + }, + { + "name": "options.disable_route_autoinstall", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sdisable-route-autoinstall + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options disable-route-autoinstall", + "result": { + "options": {"disable_route_autoinstall": True}, + }, + }, + { + "name": "options.flexvpn", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sflexvpn + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options flexvpn", + "result": { + "options": {"flexvpn": True}, + }, + }, + { + "name": "options.interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sinterface\s'?(?P<interface>\S+?)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options interface {{ interface }}", + "result": { + "options": {"interface": "{{ interface }}"}, + }, + }, + { + "name": "options.retransmission.attempts", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\sattempts\s'?(?P<attempts>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission attempts {{ attempts }}", + "result": { + "options": {"retransmission": {"attempts": "{{ attempts }}"}}, + }, + }, + { + "name": "options.retransmission.base", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\sbase\s'?(?P<base>[\d.]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission base {{ base }}", + "result": { + "options": {"retransmission": {"base": "{{ base }}"}}, + }, + }, + { + "name": "options.retransmission.timeout", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\sretransmission\stimeout\s'?(?P<timeout>\d+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options retransmission timeout {{ timeout }}", + "result": { + "options": {"retransmission": {"timeout": "{{ timeout }}"}}, + }, + }, + { + "name": "options.virtual_ip", + "getval": re.compile( + r""" + ^set\svpn\sipsec\soptions\svirtual-ip + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec options virtual-ip", + "result": { + "options": {"virtual_ip": True}, + }, + }, + { + "name": "disable_uniqreqids", + "getval": re.compile( + r""" + ^set\svpn\sipsec\sdisable-uniqreqids + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec disable-uniqreqids", + "result": { + "disable_uniqreqids": True, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py new file mode 100644 index 00000000..4319019c --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py @@ -0,0 +1,670 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Vpn_ipsec_s2sTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Vpn_ipsec_s2sTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + prefix=prefix, + ) + + # fmt: off + PARSERS = [ + { + "name": "peer", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }}", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + }, + }, + }, + }, + }, + { + "name": "peer.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} disable", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "disable": True}}}, + }, + }, + { + "name": "peer.authentication.local_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\slocal-id\s'(?P<local_id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication local-id '{{ local_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"local_id": "{{ local_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.remote_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sremote-id\s'(?P<remote_id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication remote-id '{{ remote_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"remote_id": "{{ remote_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\smode\s'(?P<mode>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication mode '{{ mode }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"mode": "{{ mode }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.use_x509_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\suse-x509-id$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication use-x509-id", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"use_x509_id": True}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sppk\sid\s'(?P<id>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk id '{{ id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"id": "{{ id }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.required", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sppk\srequired$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk required", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"required": True}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.local_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\slocal-key\s'(?P<local_key>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa local-key '{{ local_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"local_key": "{{ local_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.remote_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\sremote-key\s'(?P<remote_key>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa remote-key '{{ remote_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"remote_key": "{{ remote_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\srsa\spassphrase\s'(?P<passphrase>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\scertificate\s'(?P<certificate>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 certificate '{{ certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"certificate": "{{ certificate }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\spassphrase\s'(?P<passphrase>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.ca_certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sauthentication\sx509\sca-certificate\s'(?P<ca_certificate>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 ca-certificate '{{ ca_certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"ca_certificate": ["{{ ca_certificate }}"]}}}}, + }, + }, + }, + { + "name": "peer.childless", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \schildless\s'(?P<childless>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} childless '{{ childless }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "childless": "{{ childless }}"}}}, + }, + }, + { + "name": "peer.connection_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sconnection-type\s'(?P<connection_type>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} connection-type '{{ connection_type }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "connection_type": "{{ connection_type }}"}}}, + }, + }, + { + "name": "peer.default_esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdefault-esp-group\s'(?P<default_esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} default-esp-group '{{ default_esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "default_esp_group": "{{ default_esp_group }}"}}}, + }, + }, + { + "name": "peer.description", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdescription\s'(?P<description>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} description '{{ description }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "description": "{{ description }}"}}}, + }, + }, + { + "name": "peer.dhcp_interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sdhcp-interface\s'(?P<dhcp_interface>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} dhcp-interface '{{ dhcp_interface }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "dhcp_interface": "{{ dhcp_interface }}"}}}, + }, + }, + { + "name": "peer.force_udp_encapsulation", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sforce-udp-encapsulation$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} force-udp-encapsulation", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "force_udp_encapsulation": True}}}, + }, + }, + { + "name": "peer.ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sike-group\s'(?P<ike_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ike-group '{{ ike_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ike_group": "{{ ike_group }}"}}}, + }, + }, + { + "name": "peer.ikev2_reauth", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sikev2-reauth\s'(?P<ikev2_reauth>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ikev2-reauth '{{ ikev2_reauth }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ikev2_reauth": "{{ ikev2_reauth }}"}}}, + }, + }, + { + "name": "peer.local_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \slocal-address\s'(?P<local_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} local-address '{{ local_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "local_address": "{{ local_address }}"}}}, + }, + }, + { + "name": "peer.remote_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sremote-address\s'(?P<remote_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} remote-address '{{ remote_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "remote_address": ["{{ remote_address }}"]}}}, + }, + }, + { + "name": "peer.replay_window", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \sreplay-window\s'(?P<replay_window>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} replay-window '{{ replay_window }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "replay_window": "{{ replay_window }}"}}}, + }, + }, + { + "name": "peer.virtual_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svirtual-address\s'(?P<virtual_address>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} virtual-address '{{ virtual_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "virtual_address": ["{{ virtual_address }}"]}}}, + }, + }, + { + "name": "peer.tunnel", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }}", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} disable", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "disable": True, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sesp-group\s'(?P<esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} esp-group '{{ esp_group }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "esp_group": "{{ esp_group }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.protocol", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sprotocol\s'(?P<protocol>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} protocol '{{ protocol }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "protocol": "{{ protocol }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.priority", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\spriority\s'(?P<priority>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} priority '{{ priority }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "priority": "{{ priority }}", + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.local.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\slocal\sport\s'(?P<port>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local port '{{ port }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "local": {"port": "{{ port }}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\slocal\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.remote.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sremote\sport\s'(?P<port>\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote port '{{ port }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": { + "{{ tunnel_id }}": { + "tunnel_id": "{{ tunnel_id }}", + "remote": {"port": "{{ port }}"}, + }, + }, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \stunnel\s(?P<tunnel_id>\d+)\sremote\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.bind", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\sbind\s'(?P<bind>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti bind '{{ bind }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"bind": "{{ bind }}"}}}}, + }, + }, + { + "name": "peer.vti.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\sesp-group\s'(?P<esp_group>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti esp-group '{{ esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"esp_group": "{{ esp_group }}"}}}}, + }, + }, + { + "name": "peer.vti.traffic_selector.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\straffic-selector\slocal\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.traffic_selector.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+) + \svti\straffic-selector\sremote\sprefix\s'(?P<prefix>[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/rm_templates/vrf.py b/plugins/module_utils/network/vyos/rm_templates/vrf.py new file mode 100644 index 00000000..79928547 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vrf.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +""" +The VRF parser templates file. This contains +a list of parser definitions and associated functions that +facilitates both facts gathering and native command generation for +the given network resource. +""" + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class VrfTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(VrfTemplate, self).__init__(lines=lines, tmplt=self, prefix=prefix, module=module) + + # fmt: off + PARSERS = [ + { + "name": "table_id", + "getval": re.compile( + r""" + ^set + \s+vrf + \s+name + \s+(?P<name>\S+) + \s+table + \s+'(?P<tid>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{ name }} table {{ table_id }}", + "result": { + "name": "{{ name }}", + "table_id": "{{ tid }}", + }, + }, + { + "name": "bind_to_all", + "getval": re.compile( + r""" + ^set + \svrf + \s(?P<bta>bind-to-all) + $""", + re.VERBOSE, + ), + "setval": "vrf bind-to-all", + "result": { + "bind_to_all": "{{ True if bta is defined }}", + }, + }, + { + "name": "vni", + "getval": re.compile( + r""" + ^set + \s+vrf + \s+name + \s+(?P<name>\S+) + \s+vni + \s'(?P<vni>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} vni {{vni}}", + "result": { + "name": "{{ name }}", + "vni": "{{ vni }}", + }, + }, + { + "name": "description", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \sdescription + \s(?P<desc>\S+) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} description {{description}}", + "result": { + "name": "{{ name }}", + "description": "{{ desc }}", + }, + }, + { + "name": "disable_vrf", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<disable>disable) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} disable", + "compval": "disable", + "result": { + "name": "{{ name }}", + "disable": "{{ True if disable is defined }}", + }, + }, + # { + # "name": "address_family", + # "getval": re.compile( + # r""" + # ^set + # \svrf + # \sname + # \s(?P<name>\S+) + # \s(?P<af>ip|ipv6) + # $""", + # re.VERBOSE, + # ), + # "setval": "vrf name {{name}} {{ af }}", + # 'compval': "address_family", + # "result": { + # "name": "{{ name }}", + # "address_family": { + # '{{ "ipv4" if af == "ip" else "ipv6" }}': { + # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + # }, + # }, + # }, + # }, + # { + # "name": "address_family.disable_forwarding", + # "getval": re.compile( + # r""" + # ^set + # \svrf + # \sname + # \s(?P<name>\S+) + # \s(?P<af>ip|ipv6) + # \s(?P<df>disable-forwarding) + # $""", + # re.VERBOSE, + # ), + # "setval": "vrf name {{name}} {{ afi }} disable-forwarding", + # # "compval": "address_family.ipv6.disable_forwarding", + # "result": { + # "name": "{{ name }}", + # "address_family": { + # '{{ "ipv4" if af == "ip" else "ipv6" }}': { + # "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + # "disable_forwarding": "{{ True if df is defined }}", + # }, + # }, + # }, + # }, + { + "name": "disable_forwarding", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \s(?P<df>disable-forwarding) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} disable-forwarding", + "compval": "disable_forwarding", + "result": { + "name": "{{ name }}", + 'address_family': [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "disable_forwarding": "{{ True if df is defined }}", + }], + }, + }, + { + "name": "disable_nht", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \snht + \s(?P<nht>no-resolve-via-default) + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} nht no-resolve-via-default", + "compval": "nht_no_resolve_via_default", + "result": { + "name": "{{ name }}", + "address_family": [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "nht_no_resolve_via_default": "{{ True if nht is defined }}", + }], + }, + }, + { + "name": "route_maps", + "getval": re.compile( + r""" + ^set + \svrf + \sname + \s(?P<name>\S+) + \s(?P<af>ip|ipv6) + \sprotocol + \s(?P<proto>\S+) + \sroute-map + \s'(?P<rm>\S+)' + $""", + re.VERBOSE, + ), + "setval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }} route-map {{ route_maps.rm_name }}", + "compval": "route_maps", + "remval": "vrf name {{name}} {{ afi }} protocol {{ route_maps.protocol }}", + "result": { + "name": "{{ name }}", + "address_family": [{ + "afi": '{{ "ipv4" if af == "ip" else "ipv6" }}', + "route_maps": [{ + "rm_name": "{{ rm }}", + "protocol": "{{ proto }}", + }], + }], + }, + }, + ] + # fmt: on diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index 8722251e..89f12773 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -6,11 +6,8 @@ # utils from __future__ import absolute_import, division, print_function - __metaclass__ = type from ansible.module_utils.basic import missing_required_lib -from ansible.module_utils.six import iteritems - try: import ipaddress @@ -50,6 +47,18 @@ def get_interface_type(interface): return "dummy" +def get_interface_with_vif(interface): + """Gets virtual interface if any or return as is""" + vlan = None + interface_real = interface + if "." in interface: + interface_real, vlan = interface.split(".") + + if vlan is not None: + interface_real = interface_real + " vif " + vlan + return interface_real + + def dict_delete(base, comparable): """ This function generates a dict containing key, value pairs for keys @@ -186,7 +195,7 @@ def key_value_in_dict(have_key, have_value, want_dict): :param want_dict: :return: """ - for key, value in iteritems(want_dict): + for key, value in want_dict.items(): if key == have_key and value == have_value: return True return False @@ -258,9 +267,80 @@ def _is_w_same(w, h, key): def _in_target(h, key): """ - This function checks whether the target exist and key present in target config. + This functi checks whether the target exist and key present in target config. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h else False + + +def in_target_not_none(h, key): + """ + This function checks whether the target exist,key present in target config, and the value is not None. + :param h: target config. + :param key: attribute name. + :return: True/False. + """ + return True if h and key in h and h[key] is not None else False + + +def combine(a, b, recursive=False, list_merge="replace"): + """Merge dict ``b`` into dict ``a``, returning a new dict. + + :param a: Base dictionary. + :param b: Dictionary whose values take precedence over ``a``. + :param recursive: When True, nested dicts are merged recursively rather + than replaced wholesale. + :param list_merge: Controls how list values are combined when the same key + exists in both dicts. Supported modes: + + - ``"replace"`` *(default)* — ``b``'s list replaces ``a``'s list. + - ``"append"`` — ``b``'s list is appended to ``a``'s list (duplicates + kept). + - ``"prepend"`` — ``b``'s list is prepended to ``a``'s list + (duplicates kept). + - ``"append_rp"`` — like ``"append"`` but duplicates are removed, + preserving the first occurrence (rp = remove-preserve). + - ``"prepend_rp"`` — like ``"prepend"`` but duplicates are removed, + preserving the first occurrence. + + Passing any other value raises ``ValueError``. + :returns: New merged dict. + :raises ValueError: If either argument is not a dict, or if an + unsupported ``list_merge`` mode is given. + """ + + if not isinstance(a, dict) or not isinstance(b, dict): + raise ValueError("combine expects two dictionaries") + + result = a.copy() + + for k, v in b.items(): + if k in result: + # dict merge + if recursive and isinstance(result[k], dict) and isinstance(v, dict): + result[k] = combine(result[k], v, recursive=True, list_merge=list_merge) + + # list merge + elif isinstance(result[k], list) and isinstance(v, list): + if list_merge == "replace": + result[k] = v + elif list_merge == "append": + result[k] = result[k] + v + elif list_merge == "prepend": + result[k] = v + result[k] + elif list_merge == "append_rp": + result[k] = list(dict.fromkeys(result[k] + v)) + elif list_merge == "prepend_rp": + result[k] = list(dict.fromkeys(v + result[k])) + else: + raise ValueError(f"Unsupported list_merge mode: {list_merge}") + + # everything else + else: + result[k] = v + else: + result[k] = v + + return result diff --git a/plugins/module_utils/network/vyos/utils/version.py b/plugins/module_utils/network/vyos/utils/version.py index cc3028c3..6d84ef1c 100644 --- a/plugins/module_utils/network/vyos/utils/version.py +++ b/plugins/module_utils/network/vyos/utils/version.py @@ -7,6 +7,8 @@ """Provide version object to compare version numbers.""" from __future__ import absolute_import, division, print_function + + __metaclass__ = type diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py index 1430b1b1..4983221e 100644 --- a/plugins/module_utils/network/vyos/vyos.py +++ b/plugins/module_utils/network/vyos/vyos.py @@ -34,6 +34,7 @@ import json from ansible.module_utils._text import to_text from ansible.module_utils.connection import Connection, ConnectionError + _DEVICE_CONFIGS = {} @@ -68,8 +69,13 @@ def get_config(module, flags=None, format=None): flags = [] if flags is None else flags global _DEVICE_CONFIGS - if _DEVICE_CONFIGS != {}: - return _DEVICE_CONFIGS + # If _DEVICE_CONFIGS is non-empty and module.params["match"] is "none", + # return the cached device configurations. This avoids redundant calls + # to the connection when no specific match criteria are provided. + if _DEVICE_CONFIGS != {} and ( + module.params["match"] is not None and module.params["match"] == "none" + ): + return to_text(_DEVICE_CONFIGS) else: connection = get_connection(module) try: @@ -81,6 +87,30 @@ def get_config(module, flags=None, format=None): return cfg +def copy_file(module, source, destination, proto="scp"): + """Copy a local file to the remote device over the existing network_cli + SSH session, using netcommon's generic connection-level file transfer + RPC (the same mechanism ansible.netcommon.net_put uses). + + Requires the device to have SCP/SFTP reachable over the same SSH + session used for network_cli. Mirrors the calling convention of + cisco.iosxr's module_utils copy_file(module, source, destination, proto), + confirmed against cisco.iosxr's iosxr_config.py call site: + copy_file(module, src, dst, "sftp"). + """ + connection = get_connection(module) + try: + timeout = connection.get_option("persistent_command_timeout") + connection.copy_file( + source=source, + destination=destination, + proto=proto, + timeout=timeout, + ) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + + def run_commands(module, commands, check_rc=True): connection = get_connection(module) try: @@ -90,11 +120,16 @@ def run_commands(module, commands, check_rc=True): return response -def load_config(module, commands, commit=False, comment=None): +def load_config(module, commands, commit=False, comment=None, confirm=None): connection = get_connection(module) try: - response = connection.edit_config(candidate=commands, commit=commit, comment=comment) + response = connection.edit_config( + candidate=commands, + commit=commit, + comment=comment, + confirm=confirm, + ) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) diff --git a/plugins/module_utils/network/vyos/vyos_file.py b/plugins/module_utils/network/vyos/vyos_file.py new file mode 100644 index 00000000..2b946c20 --- /dev/null +++ b/plugins/module_utils/network/vyos/vyos_file.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2026, VyOS maintainers and contributors +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import hashlib +import re + + +STAT_RE = re.compile(r"^(?P<mode>\d+)\s+(?P<owner>\S+)\s+(?P<group>\S+)\s+(?P<size>\d+)$") + + +def parse_stat(output): + """Parse `stat --format='%a %U %G %s' <path>` output. + Returns None if the path doesn't exist (caller checks rc/stderr first). + """ + m = STAT_RE.match(output.strip()) + if not m: + return None + d = m.groupdict() + return { + "mode": d["mode"].zfill(4)[-4:], + "owner": d["owner"], + "group": d["group"], + "size": int(d["size"]), + } + + +def _normalize_mode(mode): + if mode is None: + return None + return str(mode).zfill(4)[-4:] + + +def build_want(params, local_content_hash=None): + return { + "dest": params["dest"], + "state": params.get("state", "present"), + "owner": params.get("owner"), + "group": params.get("group"), + "mode": _normalize_mode(params.get("mode")), + "content_hash": local_content_hash, + } + + +def diff_want_have(want, have): + """Returns dict of {field: (have_val, want_val)} for fields that differ. + Identity is `dest`, not a config-tree path — this compares a stat-shaped + dict, not config lines. + """ + diff = {} + if want["state"] == "absent": + if have is not None: + diff["state"] = (have, "absent") + return diff + + if have is None: + diff["state"] = (None, "present") + for f in ("owner", "group", "mode"): + if want.get(f) is not None: + diff[f] = (None, want[f]) + if want.get("content_hash"): + diff["content"] = (None, want["content_hash"]) + return diff + + for f in ("owner", "group"): + if want.get(f) is not None and want[f] != have.get(f): + diff[f] = (have.get(f), want[f]) + + if want.get("mode") is not None: + want_mode = want["mode"] + have_mode = have.get("mode") + if want_mode[0] == "0": + # Caller didn't request specific setuid/setgid/sticky bits — + # don't fight VyOS's own conventions (e.g. /config/auth is + # deliberately setgid vyattacfg; see vyos.dev T2713). Compare + # only the rwx digits unless the caller explicitly asked for a + # non-zero leading digit. + if want_mode[-3:] != have_mode[-3:]: + diff["mode"] = (have_mode, want_mode) + elif want_mode != have_mode: + diff["mode"] = (have_mode, want_mode) + + if want.get("content_hash") and want["content_hash"] != have.get("content_hash"): + diff["content"] = (have.get("content_hash"), want["content_hash"]) + + return diff + + +def local_sha256(path): + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() |
