diff --git a/README.md b/README.md index bc78a5d2..567dd6d9 100644 --- a/README.md +++ b/README.md @@ -1,264 +1,265 @@ # 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_vpn_ipsec_s2s](https://github.com/vyos/vyos.vyos/blob/main/docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst)|Manages IPsec site-to-site VPN peers on 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/docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst b/docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst new file mode 100644 index 00000000..5f177ac3 --- /dev/null +++ b/docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst @@ -0,0 +1,1218 @@ +.. _vyos.vyos.vyos_vpn_ipsec_s2s_module: + + +**************************** +vyos.vyos.vyos_vpn_ipsec_s2s +**************************** + +**Manages IPsec site-to-site VPN peers on VyOS network devices.** + + +Version added: 6.3.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module manages VPN IPsec site-to-site peer configuration on VyOS devices -- policy-based tunnels and route-based (VTI) connections. IKE/ESP groups, PSK/PPK authentication, and IPsec profiles are managed by the separate vyos_vpn_ipsec module; peers here reference those by name. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ dictionary +
+
+ +
IPsec site-to-site configuration.
+
+
+ peer + +
+ list + / elements=dictionary +
+
+ +
List of site-to-site peers.
+
+
+ authentication + +
+ dictionary +
+
+ +
Peer authentication settings.
+
+
+ local_id + +
+ string +
+
+ +
Local ID for peer authentication.
+
+
+ mode + +
+ string +
+
+
    Choices: +
  • pre-shared-secret
  • +
  • rsa
  • +
  • x509
  • +
+
+
Authentication mode.
+
+
+ ppk + +
+ dictionary +
+
+ +
Post-quantum preshared key reference for this peer.
+
+
+ id + +
+ string +
+
+ +
Post-quantum preshared key ID for this connection.
+
+
+ required + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Require a valid PPK for the connection to establish.
+
+
+ remote_id + +
+ string +
+
+ +
ID for remote authentication.
+
+
+ rsa + +
+ dictionary +
+
+ +
RSA key authentication.
+
+
+ local_key + +
+ string +
+
+ +
Name of the PKI key-pair with the local private key.
+
+
+ passphrase + +
+ string +
+
+ +
Local private key passphrase.
+
+
+ remote_key + +
+ string +
+
+ +
Name of the PKI key-pair with the remote public key.
+
+
+ use_x509_id + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Use certificate common name as ID.
+
+
+ x509 + +
+ dictionary +
+
+ +
X.509 certificate authentication.
+
+
+ ca_certificate + +
+ list + / elements=string +
+
+ +
Certificate Authority chain in PKI configuration.
+
+
+ certificate + +
+ string +
+
+ +
Certificate in PKI configuration.
+
+
+ passphrase + +
+ string +
+
+ +
Private key passphrase.
+
+
+ childless + +
+ string +
+
+
    Choices: +
  • allow
  • +
  • prefer
  • +
  • force
  • +
  • never
  • +
+
+
Childless IKE SA initiation support.
+
+
+ connection_type + +
+ string +
+
+
    Choices: +
  • initiate
  • +
  • trap
  • +
  • none
  • +
+
+
Connection type.
+
+
+ default_esp_group + +
+ string +
+
+ +
Default ESP group name for tunnels under this peer that don't specify their own.
+
+
+ description + +
+ string +
+
+ +
Description.
+
+
+ dhcp_interface + +
+ string +
+
+ +
DHCP interface supplying the next-hop IP address.
+
+
+ disable + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Disable this peer.
+
+
+ force_udp_encapsulation + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Force UDP encapsulation.
+
+
+ ike_group + +
+ string +
+
+ +
IKE group name.
+
+
+ ikev2_reauth + +
+ string +
+
+
    Choices: +
  • yes
  • +
  • no
  • +
  • inherit
  • +
+
+
Re-authentication of the remote peer during an IKE re-key (IKEv2 only).
+
+
+ local_address + +
+ string +
+
+ +
IPv4 or IPv6 address of a local interface to use for the VPN, or "any".
+
+
+ name + +
+ string + / required +
+
+ +
Connection name of the peer.
+
+
+ remote_address + +
+ list + / elements=string +
+
+ +
IPv4 or IPv6 address(es) of the remote peer, or "any".
+
+
+ replay_window + +
+ integer +
+
+ +
IPsec replay window to configure for this CHILD_SA.
+
+
+ tunnel + +
+ list + / elements=dictionary +
+
+ +
Policy-based tunnel definitions for this peer.
+
+
+ disable + +
+ boolean +
+
+
    Choices: +
  • no
  • +
  • yes
  • +
+
+
Disable this tunnel.
+
+
+ esp_group + +
+ string +
+
+ +
ESP group name for this tunnel (overrides the peer's default_esp_group).
+
+
+ local + +
+ dictionary +
+
+ +
Local traffic selector for this tunnel.
+
+
+ port + +
+ integer +
+
+ +
Local port to match.
+
+
+ prefix + +
+ list + / elements=string +
+
+ +
Local IPv4 or IPv6 prefix(es) to match.
+
+
+ priority + +
+ integer +
+
+ +
Priority for this IPsec policy (lowest value is most preferred).
+
+
+ protocol + +
+ string +
+
+ +
Protocol to match for this tunnel's traffic selector.
+
+
+ remote + +
+ dictionary +
+
+ +
Remote traffic selector for this tunnel.
+
+
+ port + +
+ integer +
+
+ +
Remote port to match.
+
+
+ prefix + +
+ list + / elements=string +
+
+ +
Remote IPv4 or IPv6 prefix(es) to match.
+
+
+ tunnel_id + +
+ integer + / required +
+
+ +
The tunnel identifier.
+
+
+ virtual_address + +
+ list + / elements=string +
+
+ +
Initiator-requested virtual address(es) from the peer.
+
+
+ vti + +
+ dictionary +
+
+ +
Route-based (VTI) connection settings for this peer.
+
+
+ bind + +
+ string +
+
+ +
VTI tunnel interface associated with this connection.
+
+
+ esp_group + +
+ string +
+
+ +
ESP group name for this VTI connection.
+
+
+ traffic_selector + +
+ dictionary +
+
+ +
Traffic selector for the VTI connection.
+
+
+ local + +
+ dictionary +
+
+ +
Local traffic-selector parameters.
+
+
+ prefix + +
+ list + / elements=string +
+
+ +
Local IPv4 or IPv6 prefix(es).
+
+
+ remote + +
+ dictionary +
+
+ +
Remote traffic-selector parameters.
+
+
+ prefix + +
+ list + / elements=string +
+
+ +
Remote IPv4 or IPv6 prefix(es).
+
+
+ running_config + +
+ string +
+
+ +
This option is used only with state parsed.
+
The value of this option should be the output received from the VyOS device by executing the command show configuration commands | match "vpn ipsec site-to-site".
+
The state parsed reads the configuration from the running_config option and transforms it into Ansible structured data as per the resource module's argspec, returned in the parsed key within the result.
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • replaced
  • +
  • overridden
  • +
  • deleted
  • +
  • gathered
  • +
  • rendered
  • +
  • parsed
  • +
+
+
The state the configuration should be left in.
+
+
+ + +Notes +----- + +.. note:: + - Tested against VyOS 1.4 and 1.5. + - Source of truth: vyos-1x's interface-definitions/vpn_ipsec.xml.in, resolved and drafted via this collection's fetch_vyos_xml_definition.py / parse_xml_definitions.py helper scripts, then hand-reviewed. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge a site-to-site peer + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + peer: + - name: PEER-TEST + ike_group: IKE-TEST + default_esp_group: ESP-TEST + remote_address: + - 203.0.113.1 + state: merged + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ after + +
+ dictionary +
+
when changed +
The resulting configuration after module execution.
+
+
+
+ before + +
+ dictionary +
+
when state is merged, replaced, overridden or deleted +
The configuration prior to the module execution.
+
+
+
+ commands + +
+ list +
+
when state is merged, replaced, overridden or deleted +
The set of commands pushed to the remote device.
+
+
+
+ gathered + +
+ dictionary +
+
when state is gathered +
Facts about the network resource gathered from the remote device as structured data.
+
+
+
+ parsed + +
+ dictionary +
+
when state is parsed +
The device native config provided in running_config option parsed into structured data as per module argspec.
+
+
+
+ rendered + +
+ list +
+
when state is rendered +
The provided configuration in the task rendered in device-native format (offline).
+
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Evgeny (@omnom62) diff --git a/meta/runtime.yml b/meta/runtime.yml index 209ed45c..7b52f11c 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,70 +1,72 @@ --- 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 + vpn_ipsec_s2s: + redirect: vyos.vyos.vyos_vpn_ipsec_s2s diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..759acee3 --- /dev/null +++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the +# cli_rm_builder. +# +# Manually editing this file is not advised. +# +# To update the argspec make the desired changes +# in the module docstring and re-run +# cli_rm_builder. +# +############################################# + +""" +The arg spec for the vyos_vpn_ipsec_s2s module +""" + + +class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903 + """The arg spec for the vyos_vpn_ipsec_s2s module""" + + argument_spec = { + "config": { + "type": "dict", + "options": { + "peer": { + "type": "list", + "elements": "dict", + "options": { + "name": {"type": "str", "required": True}, + "disable": {"type": "bool"}, + "authentication": { + "type": "dict", + "options": { + "local_id": {"type": "str"}, + "ppk": { + "type": "dict", + "options": { + "id": {"type": "str"}, + "required": {"type": "bool"}, + }, + }, + "rsa": { + "type": "dict", + "options": { + "local_key": {"type": "str"}, + "passphrase": {"type": "str", "no_log": True}, + "remote_key": {"type": "str"}, + }, + }, + "x509": { + "type": "dict", + "options": { + "certificate": {"type": "str"}, + "passphrase": {"type": "str", "no_log": True}, + "ca_certificate": { + "type": "list", + "elements": "str", + }, + }, + }, + "mode": { + "type": "str", + "choices": [ + "pre-shared-secret", + "rsa", + "x509", + ], + }, + "remote_id": {"type": "str"}, + "use_x509_id": {"type": "bool"}, + }, + }, + "childless": { + "type": "str", + "choices": ["allow", "prefer", "force", "never"], + }, + "connection_type": { + "type": "str", + "choices": ["initiate", "trap", "none"], + }, + "default_esp_group": {"type": "str"}, + "description": {"type": "str"}, + "dhcp_interface": {"type": "str"}, + "force_udp_encapsulation": {"type": "bool"}, + "ike_group": {"type": "str"}, + "ikev2_reauth": { + "type": "str", + "choices": ["yes", "no", "inherit"], + }, + "local_address": {"type": "str"}, + "remote_address": {"type": "list", "elements": "str"}, + "replay_window": {"type": "int"}, + "tunnel": { + "type": "list", + "elements": "dict", + "options": { + "tunnel_id": {"type": "int", "required": True}, + "disable": {"type": "bool"}, + "esp_group": {"type": "str"}, + "local": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "protocol": {"type": "str"}, + "priority": {"type": "int"}, + "remote": { + "type": "dict", + "options": { + "port": {"type": "int"}, + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + "virtual_address": {"type": "list", "elements": "str"}, + "vti": { + "type": "dict", + "options": { + "bind": {"type": "str"}, + "esp_group": {"type": "str"}, + "traffic_selector": { + "type": "dict", + "options": { + "local": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + "remote": { + "type": "dict", + "options": { + "prefix": { + "type": "list", + "elements": "str", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "running_config": {"type": "str"}, + "state": { + "type": "str", + "choices": [ + "merged", + "replaced", + "overridden", + "deleted", + "gathered", + "rendered", + "parsed", + ], + "default": "merged", + }, + } # pylint: disable=C0301 diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..65232ed7 --- /dev/null +++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,355 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos_vpn_ipsec_s2s config file. +It is in this file where the current configuration (as dict) +is compared to the provided configuration (as dict) and the command set +necessary to bring the current configuration to its desired end-state is +created. + +Mirrors vyos_vpn_ipsec's config.py exactly -- same list-to-dict +conversion + explicit per-state branching, same select_all/reconcile +two-flag design for the replaced/overridden distinction, same scoped +deleted handling. See that file's own docstring for the full state +semantics; the summary: + + - merged: only items/fields named in `want` are touched. + - replaced: only items NAMED in `want` are touched, but each named + item is fully reconciled (omitted fields removed). + - overridden: every item is in scope, including ones absent from + `want` -- those get deleted wholesale. Named items + reconciled the same way as replaced. + - deleted: bare (no config) deletes everything; a scoped config + deletes only what's named, down to individual list + elements. +""" + +from copy import deepcopy + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import ( + ResourceModule, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + dict_merge, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( + Facts, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2s(ResourceModule): + """ + The vyos_vpn_ipsec_s2s config class + """ + + def __init__(self, module): + super(Vpn_ipsec_s2s, self).__init__( + empty_fact_val={}, + facts_module=Facts(module), + module=module, + resource="vpn_ipsec_s2s", + tmplt=Vpn_ipsec_s2sTemplate(), + ) + + def execute_module(self): + if self.state not in ["parsed", "gathered"]: + self.generate_commands() + self.run_commands() + return self.result + + def generate_commands(self): + wantd = deepcopy(self.want) or {} + haved = deepcopy(self.have) or {} + + for entry in (wantd, haved): + self._list_to_dict(entry) + + scoped_delete = None + if self.state == "deleted": + if wantd: + scoped_delete = wantd + wantd = {} + + if self.state == "merged": + # list_to_dict() above must run BEFORE this -- dict_merge + # concatenates raw lists rather than merging matching items + # by key, so it only does the right thing once both sides + # are already name-keyed dicts. + wantd = dict_merge(haved, wantd) + + select_all = self.state in ("overridden", "deleted") + reconcile = self.state in ("replaced", "overridden", "deleted") + + self._compare_peers(wantd, haved, select_all, reconcile, scoped_delete) + + self.commands = list(dict.fromkeys(self.commands)) + + # ------------------------------------------------------------------- + # List -> name-keyed dict conversion + # ------------------------------------------------------------------- + + def _list_to_dict(self, config): + peers = config.get("peer") + if isinstance(peers, list): + config["peer"] = {p["name"]: p for p in peers} + for peer in config["peer"].values(): + if isinstance(peer.get("tunnel"), list): + peer["tunnel"] = {t["tunnel_id"]: t for t in peer["tunnel"]} + + # ------------------------------------------------------------------- + # Peers + # ------------------------------------------------------------------- + + def _compare_peers(self, wantd, haved, select_all, reconcile, scoped_delete=None): + have_peers = haved.get("peer", {}) + + if scoped_delete is not None: + for name in set(scoped_delete.get("peer", {})): + if name in have_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + return + + want_peers = wantd.get("peer", {}) + names = set(want_peers) | set(have_peers) if select_all else set(want_peers) + + for name in names: + w = want_peers.get(name, {}) + h = have_peers.get(name, {}) + if w == h: + continue + + if name in have_peers and name not in want_peers: + self.commands.append( + "delete vpn ipsec site-to-site peer {0}".format(name), + ) + continue + + if name not in have_peers: + self.addcmd({"name": name}, "peer", False) + + ctx = {"name": name} + self._cmp_bool(w, h, "disable", ctx, "peer.disable", reconcile) + + w_auth = w.get("authentication", {}) + h_auth = h.get("authentication", {}) + for field in ("local_id", "remote_id", "mode"): + self._cmp_scalar( + w_auth, + h_auth, + field, + ctx, + "peer.authentication.{0}".format(field), + reconcile, + ) + self._cmp_bool( + w_auth, + h_auth, + "use_x509_id", + ctx, + "peer.authentication.use_x509_id", + reconcile, + ) + + w_ppk = w_auth.get("ppk", {}) + h_ppk = h_auth.get("ppk", {}) + self._cmp_scalar(w_ppk, h_ppk, "id", ctx, "peer.authentication.ppk.id", reconcile) + self._cmp_bool( + w_ppk, + h_ppk, + "required", + ctx, + "peer.authentication.ppk.required", + reconcile, + ) + + w_rsa = w_auth.get("rsa", {}) + h_rsa = h_auth.get("rsa", {}) + for field in ("local_key", "remote_key", "passphrase"): + self._cmp_scalar( + w_rsa, + h_rsa, + field, + ctx, + "peer.authentication.rsa.{0}".format(field), + reconcile, + ) + + w_x509 = w_auth.get("x509", {}) + h_x509 = h_auth.get("x509", {}) + for field in ("certificate", "passphrase"): + self._cmp_scalar( + w_x509, + h_x509, + field, + ctx, + "peer.authentication.x509.{0}".format(field), + reconcile, + ) + w_ca = set(w_x509.get("ca_certificate") or []) + h_ca = set(h_x509.get("ca_certificate") or []) + for cert in w_ca - h_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + False, + ) + if reconcile: + for cert in h_ca - w_ca: + self.addcmd( + dict(ctx, ca_certificate=cert), + "peer.authentication.x509.ca_certificate", + True, + ) + + for field in ( + "childless", + "connection_type", + "default_esp_group", + "description", + "dhcp_interface", + "ike_group", + "ikev2_reauth", + "local_address", + ): + self._cmp_scalar(w, h, field, ctx, "peer.{0}".format(field), reconcile) + self._cmp_bool( + w, + h, + "force_udp_encapsulation", + ctx, + "peer.force_udp_encapsulation", + reconcile, + ) + self._cmp_scalar(w, h, "replay_window", ctx, "peer.replay_window", reconcile) + + w_remote_addr = set(w.get("remote_address") or []) + h_remote_addr = set(h.get("remote_address") or []) + for addr in w_remote_addr - h_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", False) + if reconcile: + for addr in h_remote_addr - w_remote_addr: + self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", True) + + w_virt_addr = set(w.get("virtual_address") or []) + h_virt_addr = set(h.get("virtual_address") or []) + for addr in w_virt_addr - h_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", False) + if reconcile: + for addr in h_virt_addr - w_virt_addr: + self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", True) + + self._compare_tunnels(w.get("tunnel", {}), h.get("tunnel", {}), ctx, reconcile) + self._compare_vti(w.get("vti", {}), h.get("vti", {}), ctx, reconcile) + + # ------------------------------------------------------------------- + # Tunnels (nested collection within a peer) + # ------------------------------------------------------------------- + + def _compare_tunnels(self, want_tunnels, have_tunnels, peer_ctx, reconcile): + # A tunnel collection lives entirely inside an already-selected + # peer -- once that peer is in scope, its own tunnels always get + # full reconciliation under replaced/overridden, matching how + # esp_group/ike_group's own nested proposals behave in the + # profile module. + ids = set(want_tunnels) | set(have_tunnels) if reconcile else set(want_tunnels) + for tid in ids: + w = want_tunnels.get(tid, {}) + h = have_tunnels.get(tid, {}) + if w == h: + continue + + if tid in have_tunnels and tid not in want_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", True) + continue + + if tid not in have_tunnels: + self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", False) + + ctx = dict(peer_ctx, tunnel_id=tid) + self._cmp_bool(w, h, "disable", ctx, "peer.tunnel.disable", reconcile) + for field in ("esp_group", "protocol"): + self._cmp_scalar(w, h, field, ctx, "peer.tunnel.{0}".format(field), reconcile) + self._cmp_scalar(w, h, "priority", ctx, "peer.tunnel.priority", reconcile) + + for side in ("local", "remote"): + w_side = w.get(side, {}) + h_side = h.get(side, {}) + self._cmp_scalar( + w_side, + h_side, + "port", + ctx, + "peer.tunnel.{0}.port".format(side), + reconcile, + ) + w_prefix = set(w_side.get("prefix") or []) + h_prefix = set(h_side.get("prefix") or []) + for p in w_prefix - h_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + False, + ) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd( + dict(ctx, prefix=p), + "peer.tunnel.{0}.prefix".format(side), + True, + ) + + # ------------------------------------------------------------------- + # VTI (nested dict within a peer, not a collection) + # ------------------------------------------------------------------- + + def _compare_vti(self, w_vti, h_vti, peer_ctx, reconcile): + for field in ("bind", "esp_group"): + self._cmp_scalar(w_vti, h_vti, field, peer_ctx, "peer.vti.{0}".format(field), reconcile) + + w_ts = w_vti.get("traffic_selector", {}) + h_ts = h_vti.get("traffic_selector", {}) + for side in ("local", "remote"): + w_prefix = set(w_ts.get(side, {}).get("prefix") or []) + h_prefix = set(h_ts.get(side, {}).get("prefix") or []) + parser = "peer.vti.traffic_selector.{0}.prefix".format(side) + for p in w_prefix - h_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, False) + if reconcile: + for p in h_prefix - w_prefix: + self.addcmd(dict(peer_ctx, prefix=p), parser, True) + + # ------------------------------------------------------------------- + # Field-level helpers (mirrors vyos_vpn_ipsec's own) + # ------------------------------------------------------------------- + + def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False): + w = want.get(field) + h = have.get(field) + if w != h: + if w is not None: + self.addcmd(dict(ctx, **{field: w}), parser, False) + elif reconcile and h is not None: + self.addcmd(dict(ctx, **{field: h}), parser, True) + + def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False): + explicit = field in want + w = bool(want.get(field)) + h = bool(have.get(field)) + if w != h and (w or explicit or reconcile): + self.addcmd(dict(ctx), parser, not w) diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py index 847a13f0..92eaa327 100644 --- a/plugins/module_utils/network/vyos/facts/facts.py +++ b/plugins/module_utils/network/vyos/facts/facts.py @@ -1,137 +1,141 @@ # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The facts class for vyos this file validates each subset of facts and selectively calls the appropriate facts gathering function """ from __future__ import absolute_import, division, print_function __metaclass__ = type from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts import ( FactsBase, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_address_family.bgp_address_family import ( Bgp_address_familyFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.bgp_global.bgp_global import ( Bgp_globalFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_global.firewall_global import ( Firewall_globalFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_interfaces.firewall_interfaces import ( Firewall_interfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.firewall_rules.firewall_rules import ( Firewall_rulesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.hostname.hostname import ( HostnameFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.interfaces.interfaces import ( InterfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.l3_interfaces.l3_interfaces import ( L3_interfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lag_interfaces.lag_interfaces import ( Lag_interfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.legacy.base import ( Config, Default, Neighbors, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_global.lldp_global import ( Lldp_globalFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.lldp_interfaces.lldp_interfaces import ( Lldp_interfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.logging_global.logging_global import ( Logging_globalFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ntp_global.ntp_global import ( Ntp_globalFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospf_interfaces.ospf_interfaces import ( Ospf_interfacesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv2.ospfv2 import ( Ospfv2Facts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.ospfv3.ospfv3 import ( Ospfv3Facts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.prefix_lists.prefix_lists import ( Prefix_listsFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.route_maps.route_maps import ( Route_mapsFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_server.snmp_server import ( Snmp_serverFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import ( Static_routesFacts, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec.vpn_ipsec import ( Vpn_ipsecFacts, ) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sFacts, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config) FACT_RESOURCE_SUBSETS = dict( interfaces=InterfacesFacts, l3_interfaces=L3_interfacesFacts, lag_interfaces=Lag_interfacesFacts, lldp_global=Lldp_globalFacts, lldp_interfaces=Lldp_interfacesFacts, static_routes=Static_routesFacts, firewall_rules=Firewall_rulesFacts, firewall_global=Firewall_globalFacts, firewall_interfaces=Firewall_interfacesFacts, ospfv3=Ospfv3Facts, ospfv2=Ospfv2Facts, ospf_interfaces=Ospf_interfacesFacts, bgp_global=Bgp_globalFacts, bgp_address_family=Bgp_address_familyFacts, route_maps=Route_mapsFacts, prefix_lists=Prefix_listsFacts, logging_global=Logging_globalFacts, ntp_global=Ntp_globalFacts, snmp_server=Snmp_serverFacts, hostname=HostnameFacts, vrf=VrfFacts, vpn_ipsec=Vpn_ipsecFacts, + vpn_ipsec_s2s=Vpn_ipsec_s2sFacts, ) class Facts(FactsBase): """The fact class for vyos""" VALID_LEGACY_GATHER_SUBSETS = frozenset(FACT_LEGACY_SUBSETS.keys()) VALID_RESOURCE_SUBSETS = frozenset(FACT_RESOURCE_SUBSETS.keys()) def __init__(self, module): super(Facts, self).__init__(module) def get_facts(self, legacy_facts_type=None, resource_facts_type=None, data=None): """Collect the facts for vyos :param legacy_facts_type: List of legacy facts types :param resource_facts_type: List of resource fact types :param data: previously collected conf :rtype: dict :return: the facts gathered """ if self.VALID_RESOURCE_SUBSETS: self.get_network_resources_facts(FACT_RESOURCE_SUBSETS, resource_facts_type, data) if self.VALID_LEGACY_GATHER_SUBSETS: self.get_network_legacy_facts(FACT_LEGACY_SUBSETS, legacy_facts_type) return self.ansible_facts, self._warnings diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py new file mode 100644 index 00000000..6c9b5d8e --- /dev/null +++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py @@ -0,0 +1,115 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +""" +The vyos vpn_ipsec_s2s fact class +It is in this file the configuration is collected from the device +for a given resource, parsed, and the facts tree is populated +based on the configuration. + +Follows the established per-key conversion convention used by +vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts() +naming each name-keyed dict that needs converting to a list), matching +the config.py convention for this module, rather than a generic +argspec-driven walker. +""" + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( + utils, +) + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sTemplate, +) + + +class Vpn_ipsec_s2sFacts(object): + """The vyos vpn_ipsec_s2s facts class""" + + def __init__(self, module, subspec="config", options="options"): + self._module = module + self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec + + def get_vpn_ipsec_s2s_data(self, connection): + return connection.get( + 'show configuration commands | match "vpn ipsec site-to-site"', + ) + + def process_facts(self, objFinal): + """Convert the name-keyed dicts produced by the parser into the + lists the argspec expects. + + NOTE: every PARSERS result template in rm_templates.py nests its + output under "site_to_site" -> "peer" (mirroring the CLI's own + tree: `vpn ipsec site-to-site peer ...`), but the + argspec's `config` has `peer` directly at the top level -- there + is no `site_to_site` wrapper in the argspec, since that's the + one node wrap_docstring.py unwrapped when building the + docstring (its own children became config's children directly). + So this needs to strip that outer key, not just convert the + name-keyed dicts to lists. + """ + if not objFinal: + return objFinal + + site_to_site = objFinal.get("site_to_site", {}) + peers = site_to_site.get("peer", {}) + + items = list(peers.values()) + for item in items: + if "tunnel" in item: + item["tunnel"] = sorted( + item["tunnel"].values(), + key=lambda t: int(t["tunnel_id"]), + ) + + return {"peer": sorted(items, key=lambda item: item["name"])} + + def populate_facts(self, connection, ansible_facts, data=None): + """Populate the facts for Vpn_ipsec_s2s network resource + + :param connection: the device connection + :param ansible_facts: Facts dictionary + :param data: previously collected conf + + :rtype: dictionary + :returns: facts + """ + facts = {} + + if not data: + data = self.get_vpn_ipsec_s2s_data(connection) + + vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate( + lines=data.splitlines(), + module=self._module, + ) + objs = vpn_ipsec_s2s_parser.parse() + + ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None) + objs = self.process_facts(objs) + + params = utils.remove_empties( + vpn_ipsec_s2s_parser.validate_config( + self.argument_spec, + {"config": objs}, + redact=True, + ), + ) + + facts["vpn_ipsec_s2s"] = params.get("config", {}) + ansible_facts["ansible_network_resources"].update(facts) + + return ansible_facts diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py new file mode 100644 index 00000000..8897bb0a --- /dev/null +++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py @@ -0,0 +1,610 @@ +# +# -*- coding: utf-8 -*- +# Copyright 2026 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import re + +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import ( + NetworkTemplate, +) + + +class Vpn_ipsec_s2sTemplate(NetworkTemplate): + def __init__(self, lines=None, module=None): + prefix = {"set": "set", "remove": "delete"} + super(Vpn_ipsec_s2sTemplate, self).__init__( + lines=lines, + tmplt=self, + module=module, + prefix=prefix, + ) + + # fmt: off + PARSERS = [ + { + "name": "peer", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }}", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + }, + }, + }, + }, + }, + { + "name": "peer.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} disable", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "disable": True}}}, + }, + }, + { + "name": "peer.authentication.local_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\slocal-id\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication local-id '{{ local_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"local_id": "{{ local_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.remote_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sremote-id\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication remote-id '{{ remote_id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"remote_id": "{{ remote_id }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.mode", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\smode\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication mode '{{ mode }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"mode": "{{ mode }}"}}}, + }, + }, + }, + { + "name": "peer.authentication.use_x509_id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\suse-x509-id$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication use-x509-id", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"use_x509_id": True}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.id", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sppk\sid\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk id '{{ id }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"id": "{{ id }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.ppk.required", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sppk\srequired$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk required", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"required": True}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.local_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\srsa\slocal-key\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa local-key '{{ local_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"local_key": "{{ local_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.remote_key", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\srsa\sremote-key\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa remote-key '{{ remote_key }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"remote_key": "{{ remote_key }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.rsa.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\srsa\spassphrase\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sx509\scertificate\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 certificate '{{ certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"certificate": "{{ certificate }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.passphrase", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sx509\spassphrase\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 passphrase '{{ passphrase }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"passphrase": "{{ passphrase }}"}}}}, + }, + }, + }, + { + "name": "peer.authentication.x509.ca_certificate", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sauthentication\sx509\sca-certificate\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 ca-certificate '{{ ca_certificate }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"ca_certificate": ["{{ ca_certificate }}"]}}}}, + }, + }, + }, + { + "name": "peer.childless", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \schildless\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} childless '{{ childless }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "childless": "{{ childless }}"}}}, + }, + }, + { + "name": "peer.connection_type", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sconnection-type\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} connection-type '{{ connection_type }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "connection_type": "{{ connection_type }}"}}}, + }, + }, + { + "name": "peer.default_esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sdefault-esp-group\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} default-esp-group '{{ default_esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "default_esp_group": "{{ default_esp_group }}"}}}, + }, + }, + { + "name": "peer.description", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sdescription\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} description '{{ description }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "description": "{{ description }}"}}}, + }, + }, + { + "name": "peer.dhcp_interface", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sdhcp-interface\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} dhcp-interface '{{ dhcp_interface }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "dhcp_interface": "{{ dhcp_interface }}"}}}, + }, + }, + { + "name": "peer.force_udp_encapsulation", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sforce-udp-encapsulation$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} force-udp-encapsulation", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "force_udp_encapsulation": True}}}, + }, + }, + { + "name": "peer.ike_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sike-group\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ike-group '{{ ike_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ike_group": "{{ ike_group }}"}}}, + }, + }, + { + "name": "peer.ikev2_reauth", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sikev2-reauth\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} ikev2-reauth '{{ ikev2_reauth }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ikev2_reauth": "{{ ikev2_reauth }}"}}}, + }, + }, + { + "name": "peer.local_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \slocal-address\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} local-address '{{ local_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "local_address": "{{ local_address }}"}}}, + }, + }, + { + "name": "peer.remote_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sremote-address\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} remote-address '{{ remote_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "remote_address": ["{{ remote_address }}"]}}}, + }, + }, + { + "name": "peer.replay_window", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \sreplay-window\s'(?P\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} replay-window '{{ replay_window }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "replay_window": "{{ replay_window }}"}}}, + }, + }, + { + "name": "peer.virtual_address", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \svirtual-address\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} virtual-address '{{ virtual_address }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "virtual_address": ["{{ virtual_address }}"]}}}, + }, + }, + { + "name": "peer.tunnel", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }}", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.disable", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\sdisable$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} disable", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "disable": True}}}}, + }, + }, + }, + { + "name": "peer.tunnel.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\sesp-group\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} esp-group '{{ esp_group }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "esp_group": "{{ esp_group }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.protocol", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\sprotocol\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} protocol '{{ protocol }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "protocol": "{{ protocol }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.priority", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\spriority\s'(?P\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} priority '{{ priority }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "priority": "{{ priority }}"}}}}, + }, + }, + }, + { + "name": "peer.tunnel.local.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\slocal\sport\s'(?P\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local port '{{ port }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"port": "{{ port }}"}}}}}, + }, + }, + }, + { + "name": "peer.tunnel.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\slocal\sprefix\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.tunnel.remote.port", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\sremote\sport\s'(?P\d+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote port '{{ port }}'", + "result": { + "site_to_site": { + "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"port": "{{ port }}"}}}}}, + }, + }, + }, + { + "name": "peer.tunnel.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \stunnel\s(?P\d+)\sremote\sprefix\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.bind", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \svti\sbind\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti bind '{{ bind }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"bind": "{{ bind }}"}}}}, + }, + }, + { + "name": "peer.vti.esp_group", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \svti\sesp-group\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti esp-group '{{ esp_group }}'", + "result": { + "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"esp_group": "{{ esp_group }}"}}}}, + }, + }, + { + "name": "peer.vti.traffic_selector.local.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \svti\straffic-selector\slocal\sprefix\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector local prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"local": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + { + "name": "peer.vti.traffic_selector.remote.prefix", + "getval": re.compile( + r""" + ^set\svpn\sipsec\ssite-to-site\speer\s(?P\S+) + \svti\straffic-selector\sremote\sprefix\s'(?P[^']+)'$ + """, re.VERBOSE, + ), + "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector remote prefix '{{ prefix }}'", + "result": { + "site_to_site": { + "peer": { + "{{ name }}": { + "name": "{{ name }}", + "vti": {"traffic_selector": {"remote": {"prefix": ["{{ prefix }}"]}}}, + }, + }, + }, + }, + }, + ] + # fmt: on diff --git a/plugins/modules/vyos_vpn_ipsec_s2s.py b/plugins/modules/vyos_vpn_ipsec_s2s.py new file mode 100644 index 00000000..cb790ae1 --- /dev/null +++ b/plugins/modules/vyos_vpn_ipsec_s2s.py @@ -0,0 +1,311 @@ +#!/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_s2s +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: vyos_vpn_ipsec_s2s +short_description: Manages IPsec site-to-site VPN peers on VyOS network devices. +description: This module manages VPN IPsec site-to-site peer configuration on VyOS + devices -- policy-based tunnels and route-based (VTI) connections. IKE/ESP groups, + PSK/PPK authentication, and IPsec profiles are managed by the separate vyos_vpn_ipsec + module; peers here reference those by name. +version_added: 6.3.0 +author: Evgeny (@omnom62) +notes: + - Tested against VyOS 1.4 and 1.5. + - "Source of truth: vyos-1x's interface-definitions/vpn_ipsec.xml.in, resolved and + drafted via this collection's fetch_vyos_xml_definition.py / parse_xml_definitions.py + helper scripts, then hand-reviewed." +options: + config: + description: IPsec site-to-site configuration. + type: dict + suboptions: + peer: + description: List of site-to-site peers. + type: list + elements: dict + suboptions: + name: + description: Connection name of the peer. + type: str + required: true + disable: + description: Disable this peer. + type: bool + authentication: + description: Peer authentication settings. + type: dict + suboptions: + local_id: + description: Local ID for peer authentication. + type: str + remote_id: + description: ID for remote authentication. + type: str + mode: + description: Authentication mode. + type: str + choices: [pre-shared-secret, rsa, x509] + use_x509_id: + description: Use certificate common name as ID. + type: bool + ppk: + description: Post-quantum preshared key reference for this peer. + type: dict + suboptions: + id: + description: Post-quantum preshared key ID for this connection. + type: str + required: + description: Require a valid PPK for the connection to establish. + type: bool + rsa: + description: RSA key authentication. + type: dict + suboptions: + local_key: + description: Name of the PKI key-pair with the local private key. + type: str + remote_key: + description: Name of the PKI key-pair with the remote public key. + type: str + passphrase: + description: Local private key passphrase. + type: str + x509: + description: X.509 certificate authentication. + type: dict + suboptions: + certificate: + description: Certificate in PKI configuration. + type: str + passphrase: + description: Private key passphrase. + type: str + ca_certificate: + description: Certificate Authority chain in PKI configuration. + type: list + elements: str + childless: + description: Childless IKE SA initiation support. + type: str + choices: [allow, prefer, force, never] + connection_type: + description: Connection type. + type: str + choices: [initiate, trap, none] + default_esp_group: + description: Default ESP group name for tunnels under this peer that + don't specify their own. + type: str + description: + description: Description. + type: str + dhcp_interface: + description: DHCP interface supplying the next-hop IP address. + type: str + force_udp_encapsulation: + description: Force UDP encapsulation. + type: bool + ike_group: + description: IKE group name. + type: str + ikev2_reauth: + description: Re-authentication of the remote peer during an IKE re-key + (IKEv2 only). + type: str + choices: ["yes", "no", inherit] + local_address: + description: IPv4 or IPv6 address of a local interface to use for the + VPN, or "any". + type: str + remote_address: + description: IPv4 or IPv6 address(es) of the remote peer, or "any". + type: list + elements: str + replay_window: + description: IPsec replay window to configure for this CHILD_SA. + type: int + virtual_address: + description: Initiator-requested virtual address(es) from the peer. + type: list + elements: str + tunnel: + description: Policy-based tunnel definitions for this peer. + type: list + elements: dict + suboptions: + tunnel_id: + description: The tunnel identifier. + type: int + required: true + disable: + description: Disable this tunnel. + type: bool + esp_group: + description: ESP group name for this tunnel (overrides the peer's + default_esp_group). + type: str + protocol: + description: Protocol to match for this tunnel's traffic selector. + type: str + priority: + description: Priority for this IPsec policy (lowest value is most + preferred). + type: int + local: + description: Local traffic selector for this tunnel. + type: dict + suboptions: + port: + description: Local port to match. + type: int + prefix: + description: Local IPv4 or IPv6 prefix(es) to match. + type: list + elements: str + remote: + description: Remote traffic selector for this tunnel. + type: dict + suboptions: + port: + description: Remote port to match. + type: int + prefix: + description: Remote IPv4 or IPv6 prefix(es) to match. + type: list + elements: str + vti: + description: Route-based (VTI) connection settings for this peer. + type: dict + suboptions: + bind: + description: VTI tunnel interface associated with this connection. + type: str + esp_group: + description: ESP group name for this VTI connection. + type: str + traffic_selector: + description: Traffic selector for the VTI connection. + type: dict + suboptions: + local: + description: Local traffic-selector parameters. + type: dict + suboptions: + prefix: + description: Local IPv4 or IPv6 prefix(es). + type: list + elements: str + remote: + description: Remote traffic-selector parameters. + type: dict + suboptions: + prefix: + description: Remote IPv4 or IPv6 prefix(es). + type: list + elements: str + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the VyOS device + by executing the command B(show configuration commands | match "vpn ipsec + site-to-site"). + - The state I(parsed) reads the configuration from the C(running_config) option + and transforms it into Ansible structured data as per the resource module's + argspec, returned in the I(parsed) key within the result. + type: str + state: + description: The state the configuration should be left in. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +""" + +EXAMPLES = """ +- name: Merge a site-to-site peer + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + peer: + - name: PEER-TEST + ike_group: IKE-TEST + default_esp_group: ESP-TEST + remote_address: + - 203.0.113.1 + state: merged +""" + +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 +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict +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 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: dict +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 +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2sArgs, +) +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vpn_ipsec_s2s.vpn_ipsec_s2s import ( + Vpn_ipsec_s2s, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Vpn_ipsec_s2sArgs.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_s2s(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/defaults/main.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/defaults/main.yaml new file mode 100644 index 00000000..164afead --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/meta/main.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/meta/main.yaml new file mode 100644 index 00000000..ed97d539 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/meta/main.yaml @@ -0,0 +1 @@ +--- diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/cli.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/cli.yaml new file mode 100644 index 00000000..daccf720 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/cli.yaml @@ -0,0 +1,20 @@ +--- +- name: Collect all cli test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=ansible.netcommon.network_cli) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.network_cli + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/main.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/main.yaml new file mode 100644 index 00000000..e6378581 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run CLI tests + ansible.builtin.include_tasks: cli.yaml + tags: + - network_cli diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_parsed.cfg b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_parsed.cfg new file mode 100644 index 00000000..37b7fb4e --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_parsed.cfg @@ -0,0 +1 @@ +set vpn ipsec site-to-site peer PEER-PARSE-TEST ike-group 'IKE-TEST' diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_populate.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_populate.yaml new file mode 100644 index 00000000..b196d27a --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_populate.yaml @@ -0,0 +1,37 @@ +--- +- ansible.builtin.include_tasks: _remove_config.yaml + +# Real device dependencies discovered while building this module: +# vpn ipsec site-to-site peer references esp-group/ike-group by name +# (from the vyos_vpn_ipsec profile-layer module), and vti.bind requires +# the referenced interface to already exist -- neither is created by +# this module itself. Made self-contained here rather than assuming +# vyos_vpn_ipsec's own tests already left this state behind. +- name: PREREQ Create the esp-group/ike-group referenced by the test peer + 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 + state: merged + +- name: PREREQ Create the vti0 interface referenced by vti.bind + vyos.vyos.vyos_config: + lines: + - set interfaces vti vti0 + +- name: POPULATE Apply the provided configuration + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: + config: "{{ populate_config }}" + state: merged diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_remove_config.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_remove_config.yaml new file mode 100644 index 00000000..a5da6334 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_remove_config.yaml @@ -0,0 +1,13 @@ +--- +- name: delete the provided configuration + vyos.vyos.vyos_vpn_ipsec_s2s: + state: deleted + +- name: Remove the prerequisite esp-group/ike-group + vyos.vyos.vyos_vpn_ipsec: + state: deleted + +- name: Remove the prerequisite vti0 interface + vyos.vyos.vyos_config: + lines: + - delete interfaces vti vti0 diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/deleted.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/deleted.yaml new file mode 100644 index 00000000..dc870783 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/deleted.yaml @@ -0,0 +1,60 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: TEST - [deleted] Remove all configuration + vyos.vyos.vyos_vpn_ipsec_s2s: + state: deleted + register: result + + - name: TEST - [deleted] Assert that correct set of commands were generated + assert: + that: + - "{{ deleted['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + - result.changed == true + - result.after == deleted.after + + - name: TEST - [deleted] Remove all configuration again (IDEMPOTENT) + vyos.vyos.vyos_vpn_ipsec_s2s: + state: deleted + register: result + + - name: TEST - [deleted] Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml + +- block: + - include_tasks: _populate.yaml + + - name: TEST - [deleted scoped] Remove only PEER-EXTRA by name + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + peer: + - name: PEER-EXTRA + state: deleted + register: result + + - name: TEST - [deleted scoped] Assert only PEER-EXTRA was removed + assert: + that: + - result.commands == ["delete vpn ipsec site-to-site peer PEER-EXTRA"] + - result.changed == true + + - vyos.vyos.vyos_facts: + gather_network_resources: vpn_ipsec_s2s + + - name: TEST - [deleted scoped] Assert PEER-TEST survived + assert: + that: + - ansible_facts['network_resources']['vpn_ipsec_s2s']['peer'] | map(attribute='name') | list == ['PEER-TEST'] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/empty_config.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/empty_config.yaml new file mode 100644 index 00000000..026cf22c --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/empty_config.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s empty_config integration tests on connection={{ ansible_connection }} + +- name: Merged with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + state: merged + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state merged' + +- name: Replaced with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + state: replaced + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state replaced' + +- name: Overridden with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + state: overridden + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state overridden' + +- name: Rendered with empty config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_vpn_ipsec_s2s: + config: + state: rendered + +- assert: + that: + - result.msg == 'value of config parameter must not be empty for state rendered' + +- name: Parsed with empty running_config should give appropriate error message + register: result + ignore_errors: true + vyos.vyos.vyos_vpn_ipsec_s2s: + running_config: + state: parsed + +- assert: + that: + - result.msg == 'value of running_config parameter must not be empty for state parsed' diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/gathered.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/gathered.yaml new file mode 100644 index 00000000..9b9adfd6 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: TEST - [gathered] Gather the existing running configuration + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: + state: gathered + + - name: TEST - [gathered] Assert + assert: + that: + - result.changed == false + - result.gathered.peer | map(attribute='name') | sort | list == ['PEER-EXTRA', 'PEER-TEST'] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/merged.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/merged.yaml new file mode 100644 index 00000000..06b528fb --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/merged.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- name: PREREQ Create the esp-group/ike-group referenced by the test peer + 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 + state: merged + +- name: PREREQ Create the vti0 interface referenced by vti.bind + vyos.vyos.vyos_config: + lines: + - set interfaces vti vti0 + +- block: + - name: Merge the provided configuration with the existing running configuration + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: &id001 + config: "{{ merged.config }}" + state: merged + + - vyos.vyos.vyos_facts: + gather_network_resources: vpn_ipsec_s2s + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + - result.changed == true + - result.before == {} + + - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - result['commands'] == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/overridden.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/overridden.yaml new file mode 100644 index 00000000..04a5a990 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/overridden.yaml @@ -0,0 +1,39 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Override with only PEER-TEST named -- PEER-EXTRA must be removed + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: &id001 + config: "{{ overridden.config }}" + state: overridden + + - vyos.vyos.vyos_facts: + gather_network_resources: vpn_ipsec_s2s + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ overridden['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + - result.changed == true + + - name: Assert that only PEER-TEST remains + assert: + that: + - ansible_facts['network_resources']['vpn_ipsec_s2s']['peer'] | map(attribute='name') | list == ['PEER-TEST'] + + - name: Override with the same configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - result['commands'] == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/parsed.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/parsed.yaml new file mode 100644 index 00000000..114d19d0 --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/parsed.yaml @@ -0,0 +1,14 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s parsed integration tests on connection={{ ansible_connection }} + +- name: TEST - [parsed] Parse the commands for provided configuration + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: + running_config: "{{ lookup('file', '_parsed.cfg') }}" + state: parsed + +- assert: + that: + - result.changed == false + - parsed['after'] == result.parsed diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/rendered.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/rendered.yaml new file mode 100644 index 00000000..dd5a8ddf --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/rendered.yaml @@ -0,0 +1,15 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s rendered integration tests on connection={{ ansible_connection }} + +- block: + - name: Structure provided configuration into device specific commands + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: + config: "{{ rendered.config }}" + state: rendered + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ rendered['commands'] | symmetric_difference(result['rendered']) | length == 0 }}" diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/replaced.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/replaced.yaml new file mode 100644 index 00000000..78e4158e --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/replaced.yaml @@ -0,0 +1,42 @@ +--- +- debug: + msg: START vyos_vpn_ipsec_s2s replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _populate.yaml + +- block: + - name: Replace only the named peer -- other sections must be untouched + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: &id001 + config: "{{ replaced.config }}" + state: replaced + + - vyos.vyos.vyos_facts: + gather_network_resources: vpn_ipsec_s2s + + - name: Assert that correct set of commands were generated + assert: + that: + - "{{ replaced['commands'] | symmetric_difference(result['commands']) | length == 0 }}" + - result.changed == true + + - name: Assert that PEER-EXTRA was left completely untouched + assert: + that: + - >- + (ansible_facts['network_resources']['vpn_ipsec_s2s']['peer'] + | selectattr('name', 'equalto', 'PEER-EXTRA') | list | first) + == (populate_config.peer | selectattr('name', 'equalto', 'PEER-EXTRA') | list | first) + + - name: Replace with the same configuration (IDEMPOTENT) + register: result + vyos.vyos.vyos_vpn_ipsec_s2s: *id001 + + - name: Assert that the previous task was idempotent + assert: + that: + - result['changed'] == false + - result['commands'] == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_vpn_ipsec_s2s/vars/main.yaml b/tests/integration/targets/vyos_vpn_ipsec_s2s/vars/main.yaml new file mode 100644 index 00000000..b592b73d --- /dev/null +++ b/tests/integration/targets/vyos_vpn_ipsec_s2s/vars/main.yaml @@ -0,0 +1,257 @@ +--- +populate_config: + peer: + - name: PEER-TEST + authentication: + local_id: local@example.com + mode: pre-shared-secret + remote_id: remote@example.com + childless: prefer + connection_type: initiate + default_esp_group: ESP-TEST + description: test peer for site-to-site module + force_udp_encapsulation: true + ike_group: IKE-TEST + ikev2_reauth: inherit + local_address: any + remote_address: + - 203.0.113.1 + replay_window: 32 + virtual_address: + - 0.0.0.0 + tunnel: + - tunnel_id: 1 + esp_group: ESP-TEST + priority: 10 + protocol: tcp + local: + port: 443 + prefix: + - 10.0.0.0/24 + remote: + port: 443 + prefix: + - 10.1.0.0/24 + vti: + bind: vti0 + esp_group: ESP-TEST + traffic_selector: + local: + prefix: + - 10.2.0.0/24 + remote: + prefix: + - 10.3.0.0/24 + - name: PEER-EXTRA + # Minimal, but genuinely commit-valid -- confirmed via FOUR + # separate real commit failures that VyOS requires, on every + # site-to-site peer regardless of how minimal: authentication, a + # real remote-address, a local-address (or dhcp-interface), and + # at least one of tunnel/vti (a cross-field constraint Ansible's + # argspec has no clean way to express for nested list-of-dict + # peer items -- worth documenting in the module itself, not just + # here). + ike_group: IKE-TEST + authentication: + mode: pre-shared-secret + local_id: extra-local@example.com + remote_id: extra-remote@example.com + remote_address: + - 198.51.100.1 + local_address: any + tunnel: + - tunnel_id: 1 + esp_group: ESP-TEST + +merged: + config: + peer: + - name: PEER-TEST + authentication: + local_id: local@example.com + mode: pre-shared-secret + remote_id: remote@example.com + childless: prefer + connection_type: initiate + default_esp_group: ESP-TEST + description: test peer for site-to-site module + force_udp_encapsulation: true + ike_group: IKE-TEST + ikev2_reauth: inherit + local_address: any + remote_address: + - 203.0.113.1 + replay_window: 32 + virtual_address: + - 0.0.0.0 + tunnel: + - tunnel_id: 1 + esp_group: ESP-TEST + priority: 10 + protocol: tcp + local: + port: 443 + prefix: + - 10.0.0.0/24 + remote: + port: 443 + prefix: + - 10.1.0.0/24 + vti: + bind: vti0 + esp_group: ESP-TEST + traffic_selector: + local: + prefix: + - 10.2.0.0/24 + remote: + prefix: + - 10.3.0.0/24 + commands: + - set vpn ipsec site-to-site peer PEER-TEST + - set vpn ipsec site-to-site peer PEER-TEST authentication local-id 'local@example.com' + - set vpn ipsec site-to-site peer PEER-TEST authentication remote-id 'remote@example.com' + - set vpn ipsec site-to-site peer PEER-TEST authentication mode 'pre-shared-secret' + - set vpn ipsec site-to-site peer PEER-TEST childless 'prefer' + - set vpn ipsec site-to-site peer PEER-TEST connection-type 'initiate' + - set vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST' + - set vpn ipsec site-to-site peer PEER-TEST description 'test peer for site-to-site module' + - set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST' + - set vpn ipsec site-to-site peer PEER-TEST ikev2-reauth 'inherit' + - set vpn ipsec site-to-site peer PEER-TEST local-address 'any' + - set vpn ipsec site-to-site peer PEER-TEST force-udp-encapsulation + - set vpn ipsec site-to-site peer PEER-TEST replay-window '32' + - set vpn ipsec site-to-site peer PEER-TEST remote-address '203.0.113.1' + - set vpn ipsec site-to-site peer PEER-TEST virtual-address '0.0.0.0' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 esp-group 'ESP-TEST' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 protocol 'tcp' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '10' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local port '443' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local prefix '10.0.0.0/24' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote port '443' + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote prefix '10.1.0.0/24' + - set vpn ipsec site-to-site peer PEER-TEST vti bind 'vti0' + - set vpn ipsec site-to-site peer PEER-TEST vti esp-group 'ESP-TEST' + - set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector local prefix '10.2.0.0/24' + - set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector remote prefix '10.3.0.0/24' + +replaced: + config: + peer: + - name: PEER-TEST + authentication: + local_id: local@example.com + mode: pre-shared-secret + remote_id: remote@example.com + childless: prefer + connection_type: initiate + default_esp_group: ESP-TEST + description: test peer for site-to-site module + force_udp_encapsulation: true + ike_group: IKE-TEST + ikev2_reauth: inherit + local_address: any + remote_address: + - 203.0.113.1 + replay_window: 32 + virtual_address: + - 0.0.0.0 + tunnel: + - tunnel_id: 1 + esp_group: ESP-TEST + priority: 99 + protocol: tcp + local: + port: 443 + prefix: + - 10.0.0.0/24 + remote: + port: 443 + prefix: + - 10.1.0.0/24 + vti: + bind: vti0 + esp_group: ESP-TEST + traffic_selector: + local: + prefix: + - 10.2.0.0/24 + remote: + prefix: + - 10.3.0.0/24 + # Only the priority differs from populate_config -- PEER-TEST is named + # fully/exactly otherwise, so replaced's per-item reconciliation has + # nothing else to change. PEER-EXTRA is not named at all and must + # survive untouched (checked separately in replaced.yaml). + commands: + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '99' + +overridden: + config: + peer: + - name: PEER-TEST + authentication: + local_id: local@example.com + mode: pre-shared-secret + remote_id: remote@example.com + childless: prefer + connection_type: initiate + default_esp_group: ESP-TEST + description: test peer for site-to-site module + force_udp_encapsulation: true + ike_group: IKE-TEST + ikev2_reauth: inherit + local_address: any + remote_address: + - 203.0.113.1 + replay_window: 32 + virtual_address: + - 0.0.0.0 + tunnel: + - tunnel_id: 1 + esp_group: ESP-TEST + priority: 99 + protocol: tcp + local: + port: 443 + prefix: + - 10.0.0.0/24 + remote: + port: 443 + prefix: + - 10.1.0.0/24 + vti: + bind: vti0 + esp_group: ESP-TEST + traffic_selector: + local: + prefix: + - 10.2.0.0/24 + remote: + prefix: + - 10.3.0.0/24 + commands: + - delete vpn ipsec site-to-site peer PEER-EXTRA + - set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '99' + +deleted: + commands: + - delete vpn ipsec site-to-site peer PEER-TEST + - delete vpn ipsec site-to-site peer PEER-EXTRA + after: {} + +rendered: + config: + peer: + - name: PEER-RENDER-TEST + ike_group: IKE-TEST + commands: + - set vpn ipsec site-to-site peer PEER-RENDER-TEST + - set vpn ipsec site-to-site peer PEER-RENDER-TEST ike-group 'IKE-TEST' + +parsed: + after: + peer: + - name: PEER-PARSE-TEST + ike_group: IKE-TEST diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg new file mode 100644 index 00000000..0d674b8f --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg @@ -0,0 +1,26 @@ +set vpn ipsec site-to-site peer PEER-TEST authentication local-id 'local@example.com' +set vpn ipsec site-to-site peer PEER-TEST authentication mode 'pre-shared-secret' +set vpn ipsec site-to-site peer PEER-TEST authentication remote-id 'remote@example.com' +set vpn ipsec site-to-site peer PEER-TEST childless 'prefer' +set vpn ipsec site-to-site peer PEER-TEST connection-type 'initiate' +set vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST' +set vpn ipsec site-to-site peer PEER-TEST description 'test peer for site-to-site module' +set vpn ipsec site-to-site peer PEER-TEST force-udp-encapsulation +set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST' +set vpn ipsec site-to-site peer PEER-TEST ikev2-reauth 'inherit' +set vpn ipsec site-to-site peer PEER-TEST local-address 'any' +set vpn ipsec site-to-site peer PEER-TEST remote-address '203.0.113.1' +set vpn ipsec site-to-site peer PEER-TEST replay-window '32' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 esp-group 'ESP-TEST' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local port '443' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 local prefix '10.0.0.0/24' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '10' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 protocol 'tcp' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote port '443' +set vpn ipsec site-to-site peer PEER-TEST tunnel 1 remote prefix '10.1.0.0/24' +set vpn ipsec site-to-site peer PEER-TEST virtual-address '0.0.0.0' +set vpn ipsec site-to-site peer PEER-TEST vti bind 'vti0' +set vpn ipsec site-to-site peer PEER-TEST vti esp-group 'ESP-TEST' +set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector local prefix '10.2.0.0/24' +set vpn ipsec site-to-site peer PEER-TEST vti traffic-selector remote prefix '10.3.0.0/24' +set vpn ipsec site-to-site peer PEER-EXTRA ike-group 'IKE-TEST' diff --git a/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py new file mode 100644 index 00000000..014fb867 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py @@ -0,0 +1,245 @@ +# +# (c) 2026, Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +from unittest.mock import patch + +from ansible_collections.vyos.vyos.plugins.modules import vyos_vpn_ipsec_s2s +from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args + +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosVpnIpsecS2sModule(TestVyosModule): + module = vyos_vpn_ipsec_s2s + + def setUp(self): + super(TestVyosVpnIpsecS2sModule, self).setUp() + + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module_base.get_resource_connection", + ) + self.get_resource_connection_config = self.mock_get_resource_connection_config.start() + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection", + ) + self.get_resource_connection_facts = self.mock_get_resource_connection_facts.start() + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s.Vpn_ipsec_s2sFacts.get_vpn_ipsec_s2s_data", + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestVyosVpnIpsecS2sModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_execute_show_command.stop() + + # Single fixture file, reused across every test. Carries PEER-TEST + # (richly configured -- authentication, tunnel, vti, every scalar + # type) and PEER-EXTRA (minimal, to prove "unlisted peer preserved + # under replaced / removed under overridden"). + def load_fixtures(self, commands=None, filename=None): + if filename is None: + filename = "vyos_vpn_ipsec_s2s_config.cfg" + + def load_from_file(*args, **kwargs): + return load_fixture(filename) + + self.execute_show_command.side_effect = load_from_file + + # ------------------------------------------------------------------- + # merged + # ------------------------------------------------------------------- + + def test_vyos_vpn_ipsec_s2s_merged_idempotent(self): + playbook = dict( + config=dict( + peer=[ + dict( + name="PEER-TEST", + authentication=dict( + local_id="local@example.com", + mode="pre-shared-secret", + remote_id="remote@example.com", + ), + childless="prefer", + connection_type="initiate", + default_esp_group="ESP-TEST", + description="test peer for site-to-site module", + force_udp_encapsulation=True, + ike_group="IKE-TEST", + ikev2_reauth="inherit", + local_address="any", + remote_address=["203.0.113.1"], + replay_window=32, + virtual_address=["0.0.0.0"], + tunnel=[ + dict( + tunnel_id=1, + esp_group="ESP-TEST", + protocol="tcp", + priority=10, + local=dict(port=443, prefix=["10.0.0.0/24"]), + remote=dict(port=443, prefix=["10.1.0.0/24"]), + ), + ], + vti=dict( + bind="vti0", + esp_group="ESP-TEST", + traffic_selector=dict( + local=dict(prefix=["10.2.0.0/24"]), + remote=dict(prefix=["10.3.0.0/24"]), + ), + ), + ), + ], + ), + state="merged", + ) + set_module_args(playbook) + result = self.execute_module(changed=False) + self.maxDiff = None + self.assertEqual(sorted(result["commands"]), []) + + def test_vyos_vpn_ipsec_s2s_merged_new_peer_leaves_existing_untouched(self): + playbook = dict( + config=dict(peer=[dict(name="PEER-NEW", ike_group="IKE-TEST")]), + state="merged", + ) + compare_cmds = [ + "set vpn ipsec site-to-site peer PEER-NEW", + "set vpn ipsec site-to-site peer PEER-NEW ike-group 'IKE-TEST'", + ] + set_module_args(playbook) + result = self.execute_module(changed=True) + self.maxDiff = None + self.assertEqual(sorted(result["commands"]), sorted(compare_cmds)) + + # ------------------------------------------------------------------- + # replaced + # ------------------------------------------------------------------- + + def test_vyos_vpn_ipsec_s2s_replaced_only_named_peer_touched(self): + # Name PEER-TEST fully (mirroring the fixture exactly) except a + # changed tunnel priority -- PEER-EXTRA must be left alone. + playbook = dict( + config=dict( + peer=[ + dict( + name="PEER-TEST", + authentication=dict( + local_id="local@example.com", + mode="pre-shared-secret", + remote_id="remote@example.com", + ), + childless="prefer", + connection_type="initiate", + default_esp_group="ESP-TEST", + description="test peer for site-to-site module", + force_udp_encapsulation=True, + ike_group="IKE-TEST", + ikev2_reauth="inherit", + local_address="any", + remote_address=["203.0.113.1"], + replay_window=32, + virtual_address=["0.0.0.0"], + tunnel=[ + dict( + tunnel_id=1, + esp_group="ESP-TEST", + protocol="tcp", + priority=99, + local=dict(port=443, prefix=["10.0.0.0/24"]), + remote=dict(port=443, prefix=["10.1.0.0/24"]), + ), + ], + vti=dict( + bind="vti0", + esp_group="ESP-TEST", + traffic_selector=dict( + local=dict(prefix=["10.2.0.0/24"]), + remote=dict(prefix=["10.3.0.0/24"]), + ), + ), + ), + ], + ), + state="replaced", + ) + compare_cmds = ["set vpn ipsec site-to-site peer PEER-TEST tunnel 1 priority '99'"] + set_module_args(playbook) + result = self.execute_module(changed=True) + self.maxDiff = None + self.assertEqual(sorted(result["commands"]), sorted(compare_cmds)) + + # ------------------------------------------------------------------- + # overridden + # ------------------------------------------------------------------- + + def test_vyos_vpn_ipsec_s2s_overridden_removes_unlisted_peer(self): + playbook = dict( + config=dict(peer=[dict(name="PEER-TEST", ike_group="IKE-TEST")]), + state="overridden", + ) + set_module_args(playbook) + result = self.execute_module(changed=True) + self.maxDiff = None + self.assertIn("delete vpn ipsec site-to-site peer PEER-EXTRA", result["commands"]) + # PEER-TEST had far more fields than just ike_group in the + # fixture -- overridden must reconcile all of those away too, + # since only ike_group was named. + self.assertIn( + "delete vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST'", + result["commands"], + ) + + # ------------------------------------------------------------------- + # deleted -- bare and scoped + # ------------------------------------------------------------------- + + def test_vyos_vpn_ipsec_s2s_deleted_bare(self): + set_module_args(dict(state="deleted")) + result = self.execute_module(changed=True) + self.maxDiff = None + self.assertIn("delete vpn ipsec site-to-site peer PEER-TEST", result["commands"]) + self.assertIn("delete vpn ipsec site-to-site peer PEER-EXTRA", result["commands"]) + + def test_vyos_vpn_ipsec_s2s_deleted_scoped_named_peer_only(self): + set_module_args(dict(config=dict(peer=[dict(name="PEER-EXTRA")]), state="deleted")) + result = self.execute_module(changed=True) + self.maxDiff = None + self.assertEqual(result["commands"], ["delete vpn ipsec site-to-site peer PEER-EXTRA"]) + + # ------------------------------------------------------------------- + # rendered / parsed / gathered + # ------------------------------------------------------------------- + + def test_vyos_vpn_ipsec_s2s_rendered(self): + playbook = dict( + config=dict(peer=[dict(name="PEER-RENDER-TEST", ike_group="IKE-TEST")]), + state="rendered", + ) + compare_cmds = [ + "set vpn ipsec site-to-site peer PEER-RENDER-TEST", + "set vpn ipsec site-to-site peer PEER-RENDER-TEST ike-group 'IKE-TEST'", + ] + set_module_args(playbook) + result = self.execute_module() + self.maxDiff = None + self.assertEqual(sorted(result["rendered"]), sorted(compare_cmds)) + + def test_vyos_vpn_ipsec_s2s_gathered(self): + set_module_args(dict(state="gathered")) + result = self.execute_module(changed=False) + self.maxDiff = None + names = sorted(p["name"] for p in result["gathered"]["peer"]) + self.assertEqual(names, ["PEER-EXTRA", "PEER-TEST"])