diff --git a/README.md b/README.md index c453038b..bc78a5d2 100644 --- a/README.md +++ b/README.md @@ -1,263 +1,264 @@ # VyOS Collection [![codecov](https://codecov.io/gh/vyos/vyos.vyos/graph/badge.svg?token=J217GFD69W)](https://codecov.io/gh/vyos/vyos.vyos) [![CI](https://github.com/vyos/vyos.vyos/actions/workflows/tests.yml/badge.svg?branch=main&event=schedule)](https://github.com/vyos/vyos.vyos/actions/workflows/tests.yml) The Ansible VyOS collection includes a variety of Ansible content to help automate the management of VyOS network appliances. This collection has been tested against VyOS 1.3.8, 1.4.1 and the current rolling release for 1.5. Where possible, compatibility with older versions of VyOS are maintained but not guaranteed. ## Communication * Join the VyOS forum: * [FAQ](https://forum.vyos.io/faq): find answers to frequently asked questions. * [Guides and How To](https://forum.vyos.io/c/howto-guies/27): find guides and how-to articles. * [News & Announcements](https://forum.vyos.io/c/announcements/6): track project-wide announcements . ## Ansible version compatibility This collection has been tested against the following Ansible versions: **>=2.15.0**. Plugins and modules within a collection may be tested with only specific Ansible versions. A collection may contain metadata that identifies these versions. PEP440 is the schema used to describe the versions of Ansible. ### Supported connections The VyOS collection supports ``network_cli`` connections. ## Included content ### Cliconf plugins Name | Description --- | --- [vyos.vyos.vyos](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_cliconf.rst)|Use vyos cliconf to run command on VyOS platform ### Modules Name | Description --- | --- [vyos.vyos.vyos_banner](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_banner_module.rst)|Manage multiline banners on VyOS devices [vyos.vyos.vyos_bgp_address_family](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_bgp_address_family_module.rst)|BGP Address Family resource module [vyos.vyos.vyos_bgp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_bgp_global_module.rst)|BGP global resource module [vyos.vyos.vyos_command](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_command_module.rst)|Run one or more commands on VyOS devices [vyos.vyos.vyos_config](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_config_module.rst)|Manage VyOS configuration on remote device [vyos.vyos.vyos_facts](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_facts_module.rst)|Get facts about vyos devices. [vyos.vyos.vyos_firewall_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_global_module.rst)|Firewall global resource module [vyos.vyos.vyos_firewall_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_interfaces_module.rst)|Firewall interfaces resource module [vyos.vyos.vyos_firewall_rules](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_firewall_rules_module.rst)|Firewall rules resource module [vyos.vyos.vyos_hostname](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_hostname_module.rst)|Manages hostname resource module [vyos.vyos.vyos_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_interfaces_module.rst)|Manages interface attributes of VyOS network devices. [vyos.vyos.vyos_l3_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_l3_interfaces_module.rst)|Layer 3 interfaces resource module. [vyos.vyos.vyos_lag_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lag_interfaces_module.rst)|LAG interfaces resource module [vyos.vyos.vyos_lldp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_global_module.rst)|LLDP global resource module [vyos.vyos.vyos_lldp_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_lldp_interfaces_module.rst)|LLDP interfaces resource module [vyos.vyos.vyos_logging_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_logging_global_module.rst)|Logging resource module [vyos.vyos.vyos_ntp_global](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ntp_global_module.rst)|NTP global resource module [vyos.vyos.vyos_ospf_interfaces](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospf_interfaces_module.rst)|OSPF Interfaces Resource Module. [vyos.vyos.vyos_ospfv2](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospfv2_module.rst)|OSPFv2 resource module [vyos.vyos.vyos_ospfv3](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ospfv3_module.rst)|OSPFv3 resource module [vyos.vyos.vyos_ping](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_ping_module.rst)|Tests reachability using ping from VyOS network devices [vyos.vyos.vyos_prefix_lists](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_prefix_lists_module.rst)|Prefix-Lists resource module for VyOS [vyos.vyos.vyos_route_maps](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_route_maps_module.rst)|Route Map resource module [vyos.vyos.vyos_snmp_server](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_snmp_server_module.rst)|Manages snmp_server resource module [vyos.vyos.vyos_static_routes](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_static_routes_module.rst)|Static routes resource module [vyos.vyos.vyos_system](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_system_module.rst)|Run `set system` commands on VyOS devices [vyos.vyos.vyos_user](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_user_module.rst)|Manage the collection of local users on VyOS device [vyos.vyos.vyos_vlan](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_vlan_module.rst)|Manage VLANs on VyOS network devices +[vyos.vyos.vyos_vpn_ipsec](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_vpn_ipsec_module.rst)|Manages global IPsec (ike-group, esp-group, profile, authentication, options) attributes of VyOS network devices. [vyos.vyos.vyos_vrf](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_vrf_module.rst)|VRF resource module Click the ``Content`` button to see the list of content included in this collection. ## Installing this collection You can install the VyOS collection with the Ansible Galaxy CLI: ansible-galaxy collection install vyos.vyos You can also include it in a `requirements.yml` file and install it with `ansible-galaxy collection install -r requirements.yml`, using the format: ```yaml --- collections: - name: vyos.vyos ``` ## Using this collection This collection includes [network resource modules](https://docs.ansible.com/ansible/latest/network/user_guide/network_resource_modules.html). ### Using modules from the VyOS collection in your playbooks You can call modules by their Fully Qualified Collection Namespace (FQCN), such as `vyos.vyos.vyos_static_routes`. The following example task replaces configuration changes in the existing configuration on a VyOS network device, using the FQCN: ```yaml --- - name: Replace device configurations of listed static routes with provided configurations register: result vyos.vyos.vyos_static_routes: &id001 config: - address_families: - afi: ipv4 routes: - dest: 192.0.2.32/28 blackhole_config: distance: 2 next_hops: - forward_router_address: 192.0.2.7 - forward_router_address: 192.0.2.8 - forward_router_address: 192.0.2.9 state: replaced ``` **NOTE**: For Ansible 2.9, you may not see deprecation warnings when you run your playbooks with this collection. Use this documentation to track when a module is deprecated. ### See Also: * [VyOS Platform Options](https://docs.ansible.com/ansible/latest/network/user_guide/platform_vyos.html) * [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. ## Contributing to this collection We welcome community contributions to this collection. If you find problems, please open an issue or create a PR against the [VyOS collection repository](https://github.com/vyos/vyos.vyos). See [Contributing to VyOS](https://vyos.net/contribute/) for complete details. You can also join us on: - Forum - https://forum.vyos.io See the [Contributing to VyOS](https://vyos.net/contribute/) for details on contributing to Ansible. ### Code of Conduct This collection follows the Ansible project's [Code of Conduct](https://docs.ansible.com/ansible/devel/community/code_of_conduct.html). Please read and familiarize yourself with this document. ### Updating from resource module models Some of our modules were templated using `resource_module_builder`, but some use the newer [`cli_rm_builder`](https://github.com/ansible-network/cli_rm_builder) which tempaltes baed on in-place device information, but also uses a new network parsing engine designed to simplify and standardize the parsing of network configuration. #### Using older *resource_module_builder* modules Last build was with a slightly-modified version of resource_module_builder. This changes the calling parameters for the resources. To update the collection from the resource module models, run the following command: ```bash ansible-playbook -e rm_dest=`pwd` \ -e structure=collection \ -e collection_org=vyos \ -e collection_name=vyos \ -e model=../../../resource_module_models/models/vyos/firewall_rules/vyos_firewall_rules.yaml \ ../../../resource_module_builder/site.yml ``` #### Using *cli_rm_builder* modules The newer `cli_rm_builder` works similarly to the older `resource_module_builder`, but pulls the information directly from the `DOCUMENTATION`, `EXAMPLES` and `RETURN` blocks in the module itself. To update the collection from the `cli_rm_builder` models, run the following command: ```bash ansible-playbook -e rm_dest=`pwd` \ -e collection_org=vyos \ -e collection_name=vyos \ -e resource=bgp_address_family \ ../../../cli_rm_builder/run.yml ``` Unlike the `resource_module_builder`, the `cli_rm_builder` does not require the `model` parameter. Instead, it uses the `resource` parameter to specify the resource to build. ### Testing playbooks You can use `ANSIBLE_COLLECTIONS_PATH` to test the collection locally. For example: ``` ANSIBLE_COLLECTIONS_PATHS=~/my_dev_path ansible-playbook -i inventory.network test.yml ``` ### Integration Tests Integration tests are run using `ansible-test` and require that there be an inventory defined (you can pass this in with `--inventory `) and that the system be configured for access (recommended to use SSH keys). Additionally: - eth0 should be configured for `address dhcp` and should have an assigned address on the local network - eth1 and eth2 should be defined and uncofirgured (they'll be overwritten by the tests) - eth3 and beyond should not be present or interface-related tests will fail - when using VMs for testing, ensure that the interfaces don't use `virtio`, as it will supress some interface configurations. `e1000e` is a good choice for testing. - eth0 is also expected to show `duplex auto` and `speed auto` in the output of `show interfaces`, however others are not due to the fact that they are repeatedly deleted and recreated which causes the default values to be hidden. ## Changelogs Change logs are available [here](https://github.com/vyos/vyos.vyos/blob/main/CHANGELOG.rst). ## Release notes Release notes are available [here](https://github.com/vyos/vyos.vyos/blob/main/CHANGELOG.rst). ## Roadmap Major Version | Ansible Support | VyOS Support | Details --- | --- | --- | --- 4.1.0 | 2.15 | 1.1.2 | Final release for the 4.x series 5.0.0 | 2.16 | 1.1.2 | First relase under VyOS control as a separate collection 6.0.0 | 2.18 | 1.3.8 | *Planned* release for supporting VyOS 1.3.8+ 7.0.0 | x.xx | 1.4.x | *Prospective* release deprecating incompatible 1.3.x modules Note: - Unreleased versions are not guaranteed to be released as described. - Some modules may support a wider variety of versions depending upon the compatibility with prior versions of VyOS. - The roadmap is subject to change based on community feedback and contributions. ## More information VyOS resources - [Contributing to VyOS](https://vyos.net/contribute) - [VyOS documentation](https://docs.vyos.io/en/latest/) - [VyOS forum](https://forum.vyos.io) Ansible Resources - [Ansible network resources](https://docs.ansible.com/ansible/latest/network/getting_started/network_resources.html) - [Ansible Collection overview](https://github.com/ansible-collections/overview) - [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html) - [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html) - [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) ## Licensing GNU General Public License v3.0 or later. See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text. diff --git a/changelogs/fragments/T8321_vpn_ipsec.yml b/changelogs/fragments/T8321_vpn_ipsec.yml index 2fc5abb6..abc05912 100644 --- a/changelogs/fragments/T8321_vpn_ipsec.yml +++ b/changelogs/fragments/T8321_vpn_ipsec.yml @@ -1,3 +1,3 @@ --- minor_changes: - - vyos_vrf - Add VRF support for the collection. + - vyos_vpn_ipsec - Add VPN IPSEC support for the collection. diff --git a/meta/runtime.yml b/meta/runtime.yml index f0a53ee7..209ed45c 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,68 +1,70 @@ --- requires_ansible: ">=2.15.0" plugin_routing: modules: banner: redirect: vyos.vyos.vyos_banner bgp_global: redirect: vyos.vyos.vyos_bgp_global bgp_address_family: redirect: vyos.vyos.vyos_bgp_address_family command: redirect: vyos.vyos.vyos_command config: redirect: vyos.vyos.vyos_config facts: redirect: vyos.vyos.vyos_facts firewall_global: redirect: vyos.vyos.vyos_firewall_global firewall_interfaces: redirect: vyos.vyos.vyos_firewall_interfaces firewall_rules: redirect: vyos.vyos.vyos_firewall_rules hostname: redirect: vyos.vyos.vyos_hostname interfaces: redirect: vyos.vyos.vyos_interfaces l3_interfaces: redirect: vyos.vyos.vyos_l3_interfaces lag_interfaces: redirect: vyos.vyos.vyos_lag_interfaces lldp_global: redirect: vyos.vyos.vyos_lldp_global lldp_interfaces: redirect: vyos.vyos.vyos_lldp_interfaces logging: tombstone: removal_version: 6.0.0 warning_text: use vyos_logging_global instead vyos_logging: tombstone: removal_version: 6.0.0 warning_text: use vyos_logging_global instead logging_global: redirect: vyos.vyos.vyos_logging_global ntp_global: redirect: vyos.vyos.vyos_ntp_global ospfv2: redirect: vyos.vyos.vyos_ospfv2 ospfv3: redirect: vyos.vyos.vyos_ospfv3 ospf_interfaces: redirect: vyos.vyos.vyos_ospf_interfaces ping: redirect: vyos.vyos.vyos_ping prefix_lists: redirect: vyos.vyos.vyos_prefix_lists snmp_server: redirect: vyos.vyos.vyos_snmp_server static_routes: redirect: vyos.vyos.vyos_static_routes system: redirect: vyos.vyos.vyos_system user: redirect: vyos.vyos.vyos_user vlan: redirect: vyos.vyos.vyos_vlan vrf: redirect: vyos.vyos.vyos_vrf + vpn_ipsec: + redirect: vyos.vyos.vyos_vpn_ipsec 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 index 0d9c61b3..809aad34 100644 --- a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py @@ -1,445 +1,223 @@ # -*- 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", - "choices": [ - 1, - 2, - 5, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - ], }, "encryption": { "type": "str", - "choices": [ - "null", - "aes128", - "aes192", - "aes256", - "aes128ctr", - "aes192ctr", - "aes256ctr", - "aes128ccm64", - "aes192ccm64", - "aes256ccm64", - "aes128ccm96", - "aes192ccm96", - "aes256ccm96", - "aes128ccm128", - "aes192ccm128", - "aes256ccm128", - "aes128gcm64", - "aes192gcm64", - "aes256gcm64", - "aes128gcm96", - "aes192gcm96", - "aes256gcm96", - "aes128gcm128", - "aes192gcm128", - "aes256gcm128", - "aes128gmac", - "aes192gmac", - "aes256gmac", - "3des", - "blowfish128", - "blowfish192", - "blowfish256", - "camellia128", - "camellia192", - "camellia256", - "camellia128ctr", - "camellia192ctr", - "camellia256ctr", - "camellia128ccm64", - "camellia192ccm64", - "camellia256ccm64", - "camellia128ccm96", - "camellia192ccm96", - "camellia256ccm96", - "camellia128ccm128", - "camellia192ccm128", - "camellia256ccm128", - "serpent128", - "serpent192", - "serpent256", - "twofish128", - "twofish192", - "twofish256", - "cast128", - "chacha20poly1305", - ], }, "hash": { "type": "str", - "choices": [ - "md5", - "md5_128", - "sha1", - "sha1_160", - "sha256", - "sha256_96", - "sha384", - "sha512", - "aesxcbc", - "aescmac", - "aes128gmac", - "aes192gmac", - "aes256gmac", - ], }, "prf": { "type": "str", - "choices": [ - "prfmd5", - "prfsha1", - "prfaesxcbc", - "prfaescmac", - "prfsha256", - "prfsha384", - "prfsha512", - ], }, }, }, }, }, "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", - "choices": [ - "enable", - "disable", - "dh-group1", - "dh-group2", - "dh-group5", - "dh-group14", - "dh-group15", - "dh-group16", - "dh-group17", - "dh-group18", - "dh-group19", - "dh-group20", - "dh-group21", - "dh-group22", - "dh-group23", - "dh-group24", - "dh-group25", - "dh-group26", - "dh-group27", - "dh-group28", - "dh-group29", - "dh-group30", - "dh-group31", - "dh-group32", - ], }, "proposal": { "type": "list", "elements": "dict", "options": { "proposal_id": {"type": "int"}, "encryption": { "type": "str", - "choices": [ - "null", - "aes128", - "aes192", - "aes256", - "aes128ctr", - "aes192ctr", - "aes256ctr", - "aes128ccm64", - "aes192ccm64", - "aes256ccm64", - "aes128ccm96", - "aes192ccm96", - "aes256ccm96", - "aes128ccm128", - "aes192ccm128", - "aes256ccm128", - "aes128gcm64", - "aes192gcm64", - "aes256gcm64", - "aes128gcm96", - "aes192gcm96", - "aes256gcm96", - "aes128gcm128", - "aes192gcm128", - "aes256gcm128", - "aes128gmac", - "aes192gmac", - "aes256gmac", - "3des", - "blowfish128", - "blowfish192", - "blowfish256", - "camellia128", - "camellia192", - "camellia256", - "camellia128ctr", - "camellia192ctr", - "camellia256ctr", - "camellia128ccm64", - "camellia192ccm64", - "camellia256ccm64", - "camellia128ccm96", - "camellia192ccm96", - "camellia256ccm96", - "camellia128ccm128", - "camellia192ccm128", - "camellia256ccm128", - "serpent128", - "serpent192", - "serpent256", - "twofish128", - "twofish192", - "twofish256", - "cast128", - "chacha20poly1305", - ], }, "hash": { "type": "str", - "choices": [ - "md5", - "md5_128", - "sha1", - "sha1_160", - "sha256", - "sha256_96", - "sha384", - "sha512", - "aesxcbc", - "aescmac", - "aes128gmac", - "aes192gmac", - "aes256gmac", - ], }, }, }, }, }, "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", "choices": [0, 1, 2]}, "subsystem": { "type": "list", "elements": "str", - "choices": [ - "dmn", - "mgr", - "ike", - "chd", - "job", - "cfg", - "knl", - "net", - "asn", - "enc", - "lib", - "esp", - "tls", - "tnc", - "imc", - "imv", - "pts", - "any", - ], }, }, }, "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/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py index a7847468..2d986cb4 100644 --- a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py @@ -1,115 +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. -Command generation is delegated entirely to the generic, argspec-driven -generic_compare() engine in module_utils/utils/generic_nested_resource_v2.py --- no field name (ike_group, esp_group, profile, proposal_id, encryption, -...) appears in this file. If the VyOS CLI structure changes in a future -release, only the docstring/argspec and the rm_template PARSERS need -updating; this file should not need to change for that alone. - -KNOWN GAPS (see generic_nested_resource_v2.py docstring / conversation -history for detail): - - "replaced" and "overridden" are currently treated identically. For - this singleton-style resource (one config object per device) that's - likely correct, but hasn't been deliberately confirmed against real - device behaviour the way merged/deleted/idempotency have been. - - No signature-based multi-field identity (every list[dict] node in - this module has a single clean identity field, so this hasn't been - needed yet -- would require extending generic_nested_resource_v2 if - a future nested list here doesn't). - - Bool-field handling (disable_uniqreqids, compression, disable_rekey, - etc.) is implemented but not yet exercised against real fixture data. +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_test.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( - Vpn_ipsecArgs, +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, ) -from ansible_collections.vyos.vyos_test.plugins.module_utils.network.vyos.facts.facts import ( + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( Facts, ) -from ansible_collections.vyos.vyos_test.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( Vpn_ipsecTemplate, ) -from ansible_collections.vyos.vyos_test.plugins.module_utils.network.vyos.utils.generic_nested_resource_v2 import ( - generic_compare, -) 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._spec = Vpn_ipsecArgs.argument_spec["config"]["options"] + 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. - - This is a singleton resource (one `config` dict per device, with - named collections nested inside it), not a list of top-level - named resources -- so `self.want`/`self.have` are single dicts, - not lists to key by `name`. `generic_compare()` handles the - merged/replaced/overridden/deleted/rendered branching internally - via `self.state`; this method only needs to shape `wantd` for the - "deleted" case (force empty) before handing off. """ - wantd = self.want or {} - haved = self.have or {} + 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 = {} - # NOTE: unlike merged's dict_merge in the old design, we do NOT - # merge want onto have here -- generic_compare()'s own per-item - # equality short-circuit and per-state branching (see - # generic_nested_resource_v2.generic_compare, state == "merged" - # branch: `all_ids = set(w_items)`, i.e. only touch what's named - # in want) already produces correct merged-state behaviour - # without needing a pre-merged wantd. Merging here first would - # actually break the multi-value list diffing (want would already - # contain have's values, masking real removals-within-merge). - generic_compare(self, self._spec, wantd, haved) + 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/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py index 4f7f3ba8..695b9b8d 100644 --- a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py @@ -1,102 +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_test.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( Vpn_ipsecArgs, ) -from ansible_collections.vyos.vyos_test.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import ( +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 (config: type dict, with ike_group/ - esp_group/profile/authentication.psk each: type list, elements - dict). Mirrors logging_global's hosts/files/users conversion. + 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", "profile"): + 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/rm_templates/vpn_ipse.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipse.py deleted file mode 100644 index 0a5de3f7..00000000 --- a/plugins/module_utils/network/vyos/rm_templates/vpn_ipse.py +++ /dev/null @@ -1,458 +0,0 @@ -# -*- 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. - -NOTE: this is a first pass, built only against the fixture lines -actually captured on target150 so far: - - set vpn ipsec authentication psk PSK-TEST id 'local@example.com' - set vpn ipsec authentication psk PSK-TEST id 'remote@example.com' - set vpn ipsec authentication psk PSK-TEST secret 'test-not-real-secret' - set vpn ipsec esp-group ESP-TEST proposal 1 encryption 'aes256' - set vpn ipsec esp-group ESP-TEST proposal 1 hash 'sha256' - set vpn ipsec ike-group IKE-TEST key-exchange 'ikev2' - set vpn ipsec ike-group IKE-TEST proposal 1 dh-group '14' - set vpn ipsec ike-group IKE-TEST proposal 1 encryption 'aes256' - set vpn ipsec ike-group IKE-TEST proposal 1 hash 'sha256' - set vpn ipsec profile testprofile authentication mode 'pre-shared-secret' - set vpn ipsec profile testprofile authentication pre-shared-secret 'test-not-real-secret' - set vpn ipsec profile testprofile bind tunnel 'tun0' - set vpn ipsec profile testprofile esp-group 'ESP-TEST' - set vpn ipsec profile testprofile ike-group 'IKE-TEST' - -Remaining fields from the docstring (dead_peer_detection, lifetime, mode, -pfs, disable_rekey, compression, log, options, disable_uniqreqids, -esp_group/ike_group bare-tag-only lines) are NOT covered here yet — -need a second fixture pass exercising those before this is complete. -""" - -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\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\S+) - \sproposal\s(?P\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\S+) - \sproposal\s(?P\d+) - \sencryption\s'?(?P[\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\S+) - \sproposal\s(?P\d+) - \shash\s'?(?P[\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\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\S+) - \skey-exchange\s'?(?P\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\S+) - \sproposal\s(?P\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\S+) - \sproposal\s(?P\d+) - \sdh-group\s'?(?P\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\S+) - \sproposal\s(?P\d+) - \sencryption\s'?(?P[\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\S+) - \sproposal\s(?P\d+) - \shash\s'?(?P[\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\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\S+) - \sid\s'?(?P\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\S+) - \ssecret\s'?(?P[^']+)'? - \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\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\S+) - \sauthentication\smode\s'?(?P[\w-]+)'? - \s*$""", re.VERBOSE, - ), - "setval": "vpn ipsec profile {{ name }} authentication mode {{ authentication.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\S+) - \sauthentication\spre-shared-secret\s'?(?P[^']+)'? - \s*$""", re.VERBOSE, - ), - "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ authentication.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\S+) - \sbind\stunnel\s'?(?P\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\S+) - \sesp-group\s'?(?P\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\S+) - \sike-group\s'?(?P\S+?)'? - \s*$""", re.VERBOSE, - ), - "setval": "vpn ipsec profile {{ name }} ike-group {{ ike_group }}", - "result": { - "profile": { - "{{ profile }}": { - "name": "{{ profile }}", - "ike_group": "{{ ike_group }}", - }, - }, - }, - }, - ] - # fmt: on 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..ccb63585 --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py @@ -0,0 +1,1020 @@ +# -*- 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. + +NOTE: this is a first pass, built only against the fixture lines +actually captured on target150 so far: + + set vpn ipsec authentication psk PSK-TEST id 'local@example.com' + set vpn ipsec authentication psk PSK-TEST id 'remote@example.com' + set vpn ipsec authentication psk PSK-TEST secret 'test-not-real-secret' + set vpn ipsec esp-group ESP-TEST proposal 1 encryption 'aes256' + set vpn ipsec esp-group ESP-TEST proposal 1 hash 'sha256' + set vpn ipsec ike-group IKE-TEST key-exchange 'ikev2' + set vpn ipsec ike-group IKE-TEST proposal 1 dh-group '14' + set vpn ipsec ike-group IKE-TEST proposal 1 encryption 'aes256' + set vpn ipsec ike-group IKE-TEST proposal 1 hash 'sha256' + set vpn ipsec profile testprofile authentication mode 'pre-shared-secret' + set vpn ipsec profile testprofile authentication pre-shared-secret 'test-not-real-secret' + set vpn ipsec profile testprofile bind tunnel 'tun0' + set vpn ipsec profile testprofile esp-group 'ESP-TEST' + set vpn ipsec profile testprofile ike-group 'IKE-TEST' + +Remaining fields from the docstring (dead_peer_detection, lifetime, mode, +pfs, disable_rekey, compression, log, options, disable_uniqreqids, +esp_group/ike_group bare-tag-only lines) are NOT covered here yet — +need a second fixture pass exercising those before this is complete. +""" + +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\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\S+) + \sproposal\s(?P\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\S+) + \sproposal\s(?P\d+) + \sencryption\s'?(?P[\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\S+) + \sproposal\s(?P\d+) + \shash\s'?(?P[\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\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\S+) + \skey-exchange\s'?(?P\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\S+) + \sproposal\s(?P\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\S+) + \sproposal\s(?P\d+) + \sdh-group\s'?(?P\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\S+) + \sproposal\s(?P\d+) + \sencryption\s'?(?P[\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\S+) + \sproposal\s(?P\d+) + \shash\s'?(?P[\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\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\S+) + \sid\s'?(?P\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\S+) + \ssecret\s'?(?P[^']+)'? + \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\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\S+) + \sauthentication\smode\s'?(?P[\w-]+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication mode {{ authentication.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\S+) + \sauthentication\spre-shared-secret\s'?(?P[^']+)'? + \s*$""", re.VERBOSE, + ), + "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ authentication.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\S+) + \sbind\stunnel\s'?(?P\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\S+) + \sesp-group\s'?(?P\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\S+) + \sike-group\s'?(?P\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\S+) + \sclose-action\s'?(?P[\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\S+) + \sdead-peer-detection\saction\s'?(?P[\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\S+) + \sdead-peer-detection\sinterval\s'?(?P\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\S+) + \sdead-peer-detection\stimeout\s'?(?P\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\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\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\S+) + \slifetime\s'?(?P\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\S+) + \smode\s'?(?P[\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\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\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\S+) + \slife-bytes\s'?(?P\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\S+) + \slife-packets\s'?(?P\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\S+) + \slifetime\s'?(?P\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\S+) + \smode\s'?(?P[\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\S+) + \spfs\s'?(?P[\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\S+) + \ssecret-type\s'?(?P[\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\S+) + \sdhcp-interface\s'?(?P\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\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\S+) + \sid\s'?(?P\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\S+) + \ssecret\s'?(?P[^']+)'? + \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\S+) + \ssecret-type\s'?(?P[\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\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\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\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\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\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\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[\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\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/modules/vyos_vpn_ipsec.yaml b/plugins/modules/vyos_vpn_ipsec.py similarity index 69% rename from plugins/modules/vyos_vpn_ipsec.yaml rename to plugins/modules/vyos_vpn_ipsec.py index 928544ca..0e021a04 100644 --- a/plugins/modules/vyos_vpn_ipsec.yaml +++ b/plugins/modules/vyos_vpn_ipsec.py @@ -1,480 +1,589 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for vyos_vpn_ipsec +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ module: vyos_vpn_ipsec short_description: Manages global IPsec (ike-group, esp-group, profile, authentication, options) attributes of VyOS network devices. description: This module manages global VPN IPsec configuration on VyOS devices — IKE groups, ESP groups, PSK/PPK authentication, IPsec profiles, and global options. Site-to-site peers and IKEv2 remote-access connections are handled by separate modules. version_added: 6.2.0 author: Evgeny (@omnom62) notes: - Tested against VyOS 1.4 and 1.5. - "Source of truth for field types/choices: device node.def templates under /opt/vyatta/share/vyatta-cfg/templates/vpn/ipsec/." options: config: description: IPsec global configuration. type: dict suboptions: ike_group: description: List of IKE groups. type: list elements: dict suboptions: name: description: The name of the IKE group. type: str required: true close_action: description: Action to take if a child SA is unexpectedly closed. type: str choices: [none, trap, start] dead_peer_detection: description: Dead Peer Detection (DPD). type: dict suboptions: action: description: Keep-alive failure action. type: str choices: [trap, clear, restart] interval: description: Keep-alive interval in seconds. type: int timeout: description: Dead Peer Detection keep-alive timeout (IKEv1 only), in seconds. type: int disable_mobike: description: Disable MOBIKE support (IKEv2 only). type: bool ikev2_reauth: description: Re-authentication of the remote peer during an IKE re-key (IKEv2 only). type: bool key_exchange: description: IKE version. type: str choices: [ikev1, ikev2] lifetime: description: IKE lifetime in seconds. type: int mode: description: IKEv1 phase 1 mode. type: str choices: [main, aggressive] proposal: description: List of IKE proposals. type: list elements: dict suboptions: proposal_id: description: The proposal identifier. type: int dh_group: description: Diffie-Hellman group. type: int - choices: - [ - 1, - 2, - 5, - 14, - 15, - 16, - 17, - 18, - 19, - 20, - 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - ] + choices: [1, 2, 5, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32] encryption: description: Encryption algorithm. type: str choices: - "null" - aes128 - aes192 - aes256 - aes128ctr - aes192ctr - aes256ctr - aes128ccm64 - aes192ccm64 - aes256ccm64 - aes128ccm96 - aes192ccm96 - aes256ccm96 - aes128ccm128 - aes192ccm128 - aes256ccm128 - aes128gcm64 - aes192gcm64 - aes256gcm64 - aes128gcm96 - aes192gcm96 - aes256gcm96 - aes128gcm128 - aes192gcm128 - aes256gcm128 - aes128gmac - aes192gmac - aes256gmac - 3des - blowfish128 - blowfish192 - blowfish256 - camellia128 - camellia192 - camellia256 - camellia128ctr - camellia192ctr - camellia256ctr - camellia128ccm64 - camellia192ccm64 - camellia256ccm64 - camellia128ccm96 - camellia192ccm96 - camellia256ccm96 - camellia128ccm128 - camellia192ccm128 - camellia256ccm128 - serpent128 - serpent192 - serpent256 - twofish128 - twofish192 - twofish256 - cast128 - chacha20poly1305 hash: description: Hash algorithm. type: str - choices: - [ - md5, - md5_128, - sha1, - sha1_160, - sha256, - sha256_96, - sha384, - sha512, - aesxcbc, - aescmac, - aes128gmac, - aes192gmac, - aes256gmac, - ] + choices: [md5, md5_128, sha1, sha1_160, sha256, sha256_96, sha384, sha512, aesxcbc, aescmac, aes128gmac, aes192gmac, aes256gmac] prf: description: Pseudo-Random Function. type: str - choices: - [ - prfmd5, - prfsha1, - prfaesxcbc, - prfaescmac, - prfsha256, - prfsha384, - prfsha512, - ] + choices: [prfmd5, prfsha1, prfaesxcbc, prfaescmac, prfsha256, prfsha384, prfsha512] esp_group: description: List of ESP groups. type: list elements: dict suboptions: name: description: The name of the ESP group. type: str required: true compression: description: Enable ESP compression. type: bool disable_rekey: description: Do not locally initiate a re-key of the SA; remote peer must re-key before expiration. type: bool life_bytes: description: Security Association byte count to expire. type: int life_packets: description: Security Association packet count to expire. type: int lifetime: description: Security Association time to expire, in seconds. type: int mode: description: ESP mode. type: str choices: [tunnel, transport] pfs: description: ESP Perfect Forward Secrecy. type: str choices: - enable - disable - dh-group1 - dh-group2 - dh-group5 - dh-group14 - dh-group15 - dh-group16 - dh-group17 - dh-group18 - dh-group19 - dh-group20 - dh-group21 - dh-group22 - dh-group23 - dh-group24 - dh-group25 - dh-group26 - dh-group27 - dh-group28 - dh-group29 - dh-group30 - dh-group31 - dh-group32 proposal: description: List of ESP proposals. type: list elements: dict suboptions: proposal_id: description: The proposal identifier. type: int encryption: description: Encryption algorithm. type: str choices: - "null" - aes128 - aes192 - aes256 - aes128ctr - aes192ctr - aes256ctr - aes128ccm64 - aes192ccm64 - aes256ccm64 - aes128ccm96 - aes192ccm96 - aes256ccm96 - aes128ccm128 - aes192ccm128 - aes256ccm128 - aes128gcm64 - aes192gcm64 - aes256gcm64 - aes128gcm96 - aes192gcm96 - aes256gcm96 - aes128gcm128 - aes192gcm128 - aes256gcm128 - aes128gmac - aes192gmac - aes256gmac - 3des - blowfish128 - blowfish192 - blowfish256 - camellia128 - camellia192 - camellia256 - camellia128ctr - camellia192ctr - camellia256ctr - camellia128ccm64 - camellia192ccm64 - camellia256ccm64 - camellia128ccm96 - camellia192ccm96 - camellia256ccm96 - camellia128ccm128 - camellia192ccm128 - camellia256ccm128 - serpent128 - serpent192 - serpent256 - twofish128 - twofish192 - twofish256 - cast128 - chacha20poly1305 hash: description: Hash algorithm. type: str - choices: - [ - md5, - md5_128, - sha1, - sha1_160, - sha256, - sha256_96, - sha384, - sha512, - aesxcbc, - aescmac, - aes128gmac, - aes192gmac, - aes256gmac, - ] + choices: [md5, md5_128, sha1, sha1_160, sha256, sha256_96, sha384, sha512, aesxcbc, aescmac, aes128gmac, aes192gmac, aes256gmac] authentication: description: Global pre-shared-key and post-quantum pre-shared-key definitions. type: dict suboptions: psk: description: List of pre-shared keys. type: list elements: dict suboptions: name: description: Pre-shared key name. type: str required: true id: description: ID(s) for authentication. type: list elements: str dhcp_interface: description: DHCP interface(s) supplying next-hop IP address. type: list elements: str secret: description: IKE pre-shared secret key. type: str no_log: true secret_type: description: Secret encoding type. type: str choices: [base64, hex, plaintext] ppk: description: List of post-quantum pre-shared keys. type: list elements: dict suboptions: name: description: Post-quantum pre-shared key name. type: str required: true id: description: ID(s) for PPK. type: list elements: str secret: description: Post-quantum pre-shared secret key. type: str no_log: true secret_type: description: Secret encoding type. type: str choices: [base64, hex, plaintext] profile: description: List of VPN IPsec profiles (used for e.g. DMVPN/GRE tunnel binding). type: list elements: dict suboptions: name: description: Profile name. type: str required: true authentication: type: dict suboptions: mode: description: Authentication mode. type: str choices: [pre-shared-secret] pre_shared_secret: description: Pre-shared secret key. type: str no_log: true bind_tunnel: description: Tunnel interface(s) associated with this profile. type: list elements: str disable: description: Disable this profile. type: bool esp_group: description: ESP group name to use for this profile. type: str ike_group: description: IKE group name to use for this profile. type: str interface: description: Interface(s) IPsec listens on. If omitted, listens on all interfaces. type: list elements: str log: type: dict suboptions: level: description: Global IPsec logging level. type: int choices: [0, 1, 2] subsystem: description: Per-subsystem logging levels to enable. type: list elements: str - choices: - [ - dmn, - mgr, - ike, - chd, - job, - cfg, - knl, - net, - asn, - enc, - lib, - esp, - tls, - tnc, - imc, - imv, - pts, - any, - ] + choices: [dmn, mgr, ike, chd, job, cfg, knl, net, asn, enc, lib, esp, tls, tnc, imc, imv, pts, any] options: type: dict suboptions: disable_route_autoinstall: description: Do not automatically install routes to remote networks. type: bool flexvpn: description: Allow FlexVPN vendor ID payload (IKEv2 only). type: bool interface: description: Single interface for IPsec options scope (distinct from top-level interface list). type: str retransmission: type: dict suboptions: attempts: description: Maximum number of retransmissions. type: int base: description: Base of exponential backoff. type: float timeout: description: Timeout in seconds before the first retransmission. type: int virtual_ip: description: Allow install of virtual-ip addresses. type: bool disable_uniqreqids: description: Disable requirement for unique IDs in the Security Database. type: bool state: description: The state the configuration should be left in. type: str choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] default: merged +""" + +EXAMPLES = """ +# ------------------- +# Using merged +# ------------------- + +# Before state: +# ------------- +# vyos@vyos:~$ show configuration commands | match "vpn ipsec" +# (empty) + +# Task +# ------------- +# - name: Merge provided configuration with device configuration +# vyos.vyos.vyos_vpn_ipsec: +# config: +# esp_group: +# - name: ESP-TEST +# proposal: +# - proposal_id: 1 +# encryption: aes256 +# hash: sha256 +# ike_group: +# - name: IKE-TEST +# key_exchange: ikev2 +# proposal: +# - proposal_id: 1 +# encryption: aes256 +# hash: sha256 +# dh_group: 14 +# state: merged + +# Task output: +# ------------- +# "commands": [ +# "set vpn ipsec esp-group ESP-TEST", +# "set vpn ipsec esp-group ESP-TEST proposal 1", +# "set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256", +# "set vpn ipsec esp-group ESP-TEST proposal 1 hash sha256", +# "set vpn ipsec ike-group IKE-TEST", +# "set vpn ipsec ike-group IKE-TEST key-exchange ikev2", +# "set vpn ipsec ike-group IKE-TEST proposal 1", +# "set vpn ipsec ike-group IKE-TEST proposal 1 encryption aes256", +# "set vpn ipsec ike-group IKE-TEST proposal 1 hash sha256", +# "set vpn ipsec ike-group IKE-TEST proposal 1 dh-group 14" +# ] + +# ------------------- +# Using gathered +# ------------------- + +# Task +# ------------- +# - name: Gather current vpn_ipsec configuration +# vyos.vyos.vyos_vpn_ipsec: +# state: gathered + +# ------------------- +# Using deleted +# ------------------- + +# Task +# ------------- +# - name: Remove all vpn_ipsec configuration +# vyos.vyos.vyos_vpn_ipsec: +# state: deleted + +# ------------------- +# Using rendered +# ------------------- + +# Task +# ------------- +# - name: Render configuration without touching the device +# vyos.vyos.vyos_vpn_ipsec: +# config: +# esp_group: +# - name: ESP-TEST +# proposal: +# - proposal_id: 1 +# encryption: aes256 +# hash: sha256 +# state: rendered + +# ------------------- +# Using parsed +# ------------------- + +# Task +# ------------- +# - name: Parse raw config text into structured facts +# vyos.vyos.vyos_vpn_ipsec: +# running_config: "{{ lookup('file', './vpn_ipsec.cfg') }}" +# state: parsed +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) + type: list + sample: + - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256 + - set vpn ipsec ike-group IKE-TEST key-exchange ikev2 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsecArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vpn_ipsec.vpn_ipsec import ( + Vpn_ipsec, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vpn_ipsecArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Vpn_ipsec(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main()