Page MenuHomeVyOS Platform

No OneTemporary

Size
391 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/README.md b/README.md
index c453038b..567dd6d9 100644
--- a/README.md
+++ b/README.md
@@ -1,263 +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 .
<!--start requires_ansible-->
## 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.
<!--end requires_ansible-->
### Supported connections
The VyOS collection supports ``network_cli`` connections.
## Included content
<!--start collection 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
<!--end collection content-->
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 <filename>`) 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
<!-- Optional. Include the roadmap for this collection, and the proposed release/versioning strategy so users can anticipate the upgrade/update cycle. -->
Major Version | Ansible Support | VyOS Support | Details
--- | --- | --- | ---
4.1.0 | 2.15 | 1.1.2 | Final release for the 4.x series
5.0.0 | 2.16 | 1.1.2 | First relase under VyOS control as a separate collection
6.0.0 | 2.18 | 1.3.8 | *Planned* release for supporting VyOS 1.3.8+
7.0.0 | x.xx | 1.4.x | *Prospective* release deprecating incompatible 1.3.x modules
Note:
- Unreleased versions are not guaranteed to be released as described.
- Some modules may support a wider variety of versions depending upon the compatibility
with prior versions of VyOS.
- The roadmap is subject to change based on community feedback and contributions.
## More information
VyOS resources
- [Contributing to VyOS](https://vyos.net/contribute)
- [VyOS documentation](https://docs.vyos.io/en/latest/)
- [VyOS forum](https://forum.vyos.io)
Ansible Resources
- [Ansible network resources](https://docs.ansible.com/ansible/latest/network/getting_started/network_resources.html)
- [Ansible Collection overview](https://github.com/ansible-collections/overview)
- [Ansible User guide](https://docs.ansible.com/ansible/latest/user_guide/index.html)
- [Ansible Developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html)
- [Ansible Community code of conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html)
## Licensing
GNU General Public License v3.0 or later.
See [LICENSE](https://www.gnu.org/licenses/gpl-3.0.txt) to see the full text.
diff --git a/changelogs/fragments/T8321_vpn_ipsec.yml b/changelogs/fragments/T8321_vpn_ipsec.yml
new file mode 100644
index 00000000..cc33671f
--- /dev/null
+++ b/changelogs/fragments/T8321_vpn_ipsec.yml
@@ -0,0 +1,4 @@
+---
+minor_changes:
+ - vyos_vpn_ipsec - Add global VPN IPsec resource module support.
+ - vyos_vpn_ipsec_s2s - Add IPsec site-to-site peer resource module support.
diff --git a/docs/vyos.vyos.vyos_vpn_ipsec_module.rst b/docs/vyos.vyos.vyos_vpn_ipsec_module.rst
new file mode 100644
index 00000000..79ca84b8
--- /dev/null
+++ b/docs/vyos.vyos.vyos_vpn_ipsec_module.rst
@@ -0,0 +1,1553 @@
+.. _vyos.vyos.vyos_vpn_ipsec_module:
+
+
+************************
+vyos.vyos.vyos_vpn_ipsec
+************************
+
+**Manages global IPsec (ike-group, esp-group, profile, authentication, options) attributes of VyOS network devices.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- This module manages global VPN IPsec configuration on VyOS devices -- IKE groups, ESP groups, PSK/PPK authentication, IPsec profiles, and global options. Site-to-site peers and IKEv2 remote-access connections are handled by separate modules.
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="4">Parameter</th>
+ <th>Choices/<font color="blue">Defaults</font></th>
+ <th width="100%">Comments</th>
+ </tr>
+ <tr>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPsec global configuration.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>authentication</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Global pre-shared-key and post-quantum pre-shared-key definitions.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ppk</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of post-quantum pre-shared keys.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ID(s) for PPK.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Post-quantum pre-shared key name.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>secret</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Post-quantum pre-shared secret key.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>secret_type</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>base64</li>
+ <li>hex</li>
+ <li>plaintext</li>
+ </ul>
+ </td>
+ <td>
+ <div>Secret encoding type.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>psk</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of pre-shared keys.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>dhcp_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>DHCP interface(s) supplying next-hop IP address.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ID(s) for authentication.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Pre-shared key name.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>secret</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IKE pre-shared secret key.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>secret_type</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>base64</li>
+ <li>hex</li>
+ <li>plaintext</li>
+ </ul>
+ </td>
+ <td>
+ <div>Secret encoding type.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable_uniqreqids</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable requirement for unique IDs in the Security Database.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>esp_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of ESP groups.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>compression</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Enable ESP compression.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable_rekey</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Do not locally initiate a re-key of the SA; remote peer must re-key before expiration.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>life_bytes</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Security Association byte count to expire.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>life_packets</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Security Association packet count to expire.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>lifetime</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Security Association time to expire, in seconds.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mode</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>tunnel</li>
+ <li>transport</li>
+ </ul>
+ </td>
+ <td>
+ <div>ESP mode.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The name of the ESP group.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pfs</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ESP Perfect Forward Secrecy. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side, not enumerated here since the set is version-dependent.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>proposal</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of ESP proposals.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>encryption</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Encryption algorithm. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side, not enumerated here since the set is version-dependent.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>hash</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Hash algorithm. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>proposal_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The proposal identifier.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ike_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of IKE groups.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>close_action</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>none</li>
+ <li>trap</li>
+ <li>start</li>
+ </ul>
+ </td>
+ <td>
+ <div>Action to take if a child SA is unexpectedly closed.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>dead_peer_detection</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Dead Peer Detection (DPD).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>action</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>trap</li>
+ <li>clear</li>
+ <li>restart</li>
+ </ul>
+ </td>
+ <td>
+ <div>Keep-alive failure action.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>interval</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Keep-alive interval in seconds.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>timeout</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Dead Peer Detection keep-alive timeout (IKEv1 only), in seconds.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable_mobike</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable MOBIKE support (IKEv2 only).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ikev2_reauth</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Re-authentication of the remote peer during an IKE re-key (IKEv2 only).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>key_exchange</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>ikev1</li>
+ <li>ikev2</li>
+ </ul>
+ </td>
+ <td>
+ <div>IKE version.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>lifetime</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IKE lifetime in seconds.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mode</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>main</li>
+ <li>aggressive</li>
+ </ul>
+ </td>
+ <td>
+ <div>IKEv1 phase 1 mode.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The name of the IKE group.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>proposal</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of IKE proposals.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>dh_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Diffie-Hellman group. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side, not enumerated here since the set is version-dependent.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>encryption</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Encryption algorithm. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side, not enumerated here since the set is version-dependent.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>hash</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Hash algorithm. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prf</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Pseudo-Random Function. See VyOS/strongSwan documentation for the full set of valid values -- validated device-side.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>proposal_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The proposal identifier.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Interface(s) IPsec listens on. If omitted, listens on all interfaces.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>log</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPsec logging settings.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>level</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Global IPsec logging level.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>subsystem</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Per-subsystem logging levels to enable.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>options</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Global IPsec options.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable_route_autoinstall</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Do not automatically install routes to remote networks.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>flexvpn</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Allow FlexVPN vendor ID payload (IKEv2 only).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Single interface for IPsec options scope (distinct from top-level interface list).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>retransmission</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPsec retransmission settings.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>attempts</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Maximum number of retransmissions.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>base</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">float</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Base of exponential backoff.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>timeout</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Timeout in seconds before the first retransmission.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>virtual_ip</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Allow install of virtual-ip addresses.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>profile</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of VPN IPsec profiles (used for e.g. DMVPN/GRE tunnel binding).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>authentication</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Authentication settings for this profile.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mode</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>pre-shared-secret</li>
+ </ul>
+ </td>
+ <td>
+ <div>Authentication mode.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>pre_shared_secret</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Pre-shared secret key.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>bind_tunnel</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Tunnel interface(s) associated with this profile.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this profile.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>esp_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ESP group name to use for this profile.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ike_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IKE group name to use for this profile.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Profile name.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>running_config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>This option is used only with state <em>parsed</em>.</div>
+ <div>The value of this option should be the output received from the VyOS device by executing the command <b>show configuration commands | match &quot;vpn ipsec&quot;</b>.</div>
+ <div>The states <em>replaced</em> and <em>overridden</em> have identical behaviour for this module with respect to named collections (ike_group, esp_group, profile, authentication), but differ in scope -- see the module description for detail.</div>
+ <div>The state <em>parsed</em> reads the configuration from the <code>running_config</code> option and transforms it into Ansible structured data as per the resource module&#x27;s argspec, returned in the <em>parsed</em> key within the result.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>state</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
+ <li>replaced</li>
+ <li>overridden</li>
+ <li>deleted</li>
+ <li>gathered</li>
+ <li>rendered</li>
+ <li>parsed</li>
+ </ul>
+ </td>
+ <td>
+ <div>The state the configuration should be left in.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+Notes
+-----
+
+.. note::
+ - Tested against VyOS 1.4 and 1.5.
+ - Source of truth for field types/choices: device node.def templates under /opt/vyatta/share/vyatta-cfg/templates/vpn/ipsec/.
+ - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide <network_guide>`
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ - name: Merge provided configuration with device configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-TEST
+ key_exchange: ikev2
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ dh_group: 14
+ state: merged
+
+ - name: Replace one named esp-group, leaving all other groups untouched
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+ state: replaced
+
+ - name: Override the whole configuration -- anything not listed here is removed
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ state: overridden
+
+ - name: Delete one named esp-group, leaving all other groups untouched
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ state: deleted
+
+ - name: Remove all vpn_ipsec configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ state: deleted
+
+ - name: Gather current vpn_ipsec configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ state: gathered
+
+ - name: Render configuration without touching the device
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ state: rendered
+
+ - name: Parse raw config text into structured facts
+ vyos.vyos.vyos_vpn_ipsec:
+ running_config: "{{ lookup('file', './vpn_ipsec.cfg') }}"
+ state: parsed
+
+
+
+Return Values
+-------------
+Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Key</th>
+ <th>Returned</th>
+ <th width="100%">Description</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>after</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when changed</td>
+ <td>
+ <div>The resulting configuration after module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>before</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The configuration prior to the module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>commands</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The set of commands pushed to the remote device.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&#x27;set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256&#x27;, &#x27;set vpn ipsec ike-group IKE-TEST key-exchange ikev2&#x27;]</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>gathered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>gathered</code></td>
+ <td>
+ <div>Facts about the network resource gathered from the remote device as structured data.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>parsed</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>parsed</code></td>
+ <td>
+ <div>The device native config provided in <em>running_config</em> option parsed into structured data as per module argspec.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>rendered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>rendered</code></td>
+ <td>
+ <div>The provided configuration in the task rendered in device-native format (offline).</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&#x27;set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256&#x27;]</div>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Evgeny Molotkov (@omnom62)
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..8da5dc8a
--- /dev/null
+++ b/docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst
@@ -0,0 +1,1232 @@
+.. _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: 1.0.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
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="6">Parameter</th>
+ <th>Choices/<font color="blue">Defaults</font></th>
+ <th width="100%">Comments</th>
+ </tr>
+ <tr>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPsec site-to-site configuration.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="5">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>peer</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of site-to-site peers.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>authentication</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Peer authentication settings.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>local_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local ID for peer authentication.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>mode</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>pre-shared-secret</li>
+ <li>rsa</li>
+ <li>x509</li>
+ </ul>
+ </td>
+ <td>
+ <div>Authentication mode.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ppk</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Post-quantum preshared key reference for this peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Post-quantum preshared key ID for this connection.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>required</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Require a valid PPK for the connection to establish.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>remote_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ID for remote authentication.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>rsa</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>RSA key authentication.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>local_key</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Name of the PKI key-pair with the local private key.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>passphrase</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local private key passphrase.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>remote_key</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Name of the PKI key-pair with the remote public key.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>use_x509_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Use certificate common name as ID.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>x509</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>X.509 certificate authentication.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ca_certificate</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Certificate Authority chain in PKI configuration.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>certificate</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Certificate in PKI configuration.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>passphrase</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Private key passphrase.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>childless</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>allow</li>
+ <li>prefer</li>
+ <li>force</li>
+ <li>never</li>
+ </ul>
+ </td>
+ <td>
+ <div>Childless IKE SA initiation support.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>connection_type</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>initiate</li>
+ <li>trap</li>
+ <li>none</li>
+ </ul>
+ </td>
+ <td>
+ <div>Connection type.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>default_esp_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Default ESP group name for tunnels under this peer that don&#x27;t specify their own.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Description.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>dhcp_interface</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>DHCP interface supplying the next-hop IP address.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>force_udp_encapsulation</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Force UDP encapsulation.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ike_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IKE group name.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ikev2_reauth</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>yes</li>
+ <li>no</li>
+ <li>inherit</li>
+ </ul>
+ </td>
+ <td>
+ <div>Re-authentication of the remote peer during an IKE re-key (IKEv2 only).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>local_address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv4 or IPv6 address of a local interface to use for the VPN, or &quot;any&quot;.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Connection name of the peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>remote_address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPv4 or IPv6 address(es) of the remote peer, or &quot;any&quot;.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>replay_window</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>IPsec replay window to configure for this CHILD_SA.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>tunnel</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Policy-based tunnel definitions for this peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>disable</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>no</li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Disable this tunnel.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>esp_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ESP group name for this tunnel (overrides the peer&#x27;s default_esp_group).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>local</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local traffic selector for this tunnel.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local port to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local IPv4 or IPv6 prefix(es) to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>priority</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Priority for this IPsec policy (lowest value is most preferred).</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>protocol</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Protocol to match for this tunnel&#x27;s traffic selector.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>remote</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Remote traffic selector for this tunnel.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>port</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Remote port to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Remote IPv4 or IPv6 prefix(es) to match.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>tunnel_id</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>The tunnel identifier.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>virtual_address</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Initiator-requested virtual address(es) from the peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>vti</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Route-based (VTI) connection settings for this peer.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>bind</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>VTI tunnel interface associated with this connection.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>esp_group</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>ESP group name for this VTI connection.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>traffic_selector</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Traffic selector for the VTI connection.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>local</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local traffic-selector parameters.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Local IPv4 or IPv6 prefix(es).</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>remote</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Remote traffic-selector parameters.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Remote IPv4 or IPv6 prefix(es).</div>
+ </td>
+ </tr>
+
+
+
+
+
+ <tr>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>running_config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>This option is used only with state <em>parsed</em>.</div>
+ <div>The value of this option should be the output received from the VyOS device by executing the command <b>show configuration commands | match &quot;vpn ipsec site-to-site&quot;</b>.</div>
+ <div>The state <em>parsed</em> reads the configuration from the <code>running_config</code> option and transforms it into Ansible structured data as per the resource module&#x27;s argspec, returned in the <em>parsed</em> key within the result.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="6">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>state</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
+ <li>replaced</li>
+ <li>overridden</li>
+ <li>deleted</li>
+ <li>gathered</li>
+ <li>rendered</li>
+ <li>parsed</li>
+ </ul>
+ </td>
+ <td>
+ <div>The state the configuration should be left in.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+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.
+ - The argspec only requires *name* on a peer, but VyOS itself enforces several more requirements at commit time -- confirmed via real device testing, not visible in the argspec: every peer needs ``authentication``, a real ``remote_address`` (not just omitted), a ``local_address`` or ``dhcp_interface``, and at least one of ``tunnel`` or ``vti``. A peer missing any of these will pass Ansible's own argument validation but fail the device commit with a specific error naming what's missing.
+ - For more information on using Ansible to manage network devices see the :ref:`Ansible Network Guide <network_guide>`
+
+
+
+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 <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Key</th>
+ <th>Returned</th>
+ <th width="100%">Description</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>after</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when changed</td>
+ <td>
+ <div>The resulting configuration after module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>before</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The configuration prior to the module execution.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>commands</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>
+ <div>The set of commands pushed to the remote device.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&quot;set vpn ipsec site-to-site peer PEER-TEST ike-group &#x27;IKE-TEST&#x27;&quot;, &quot;set vpn ipsec site-to-site peer PEER-TEST default-esp-group &#x27;ESP-TEST&#x27;&quot;]</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>gathered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>gathered</code></td>
+ <td>
+ <div>Facts about the network resource gathered from the remote device as structured data.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>parsed</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>parsed</code></td>
+ <td>
+ <div>The device native config provided in <em>running_config</em> option parsed into structured data as per module argspec.</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">This output will always be in the same format as the module argspec.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>rendered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when <em>state</em> is <code>rendered</code></td>
+ <td>
+ <div>The provided configuration in the task rendered in device-native format (offline).</div>
+ <br/>
+ <div style="font-size: smaller"><b>Sample:</b></div>
+ <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[&quot;set vpn ipsec site-to-site peer PEER-TEST ike-group &#x27;IKE-TEST&#x27;&quot;]</div>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- Evgeny Molotkov (@omnom62)
diff --git a/meta/runtime.yml b/meta/runtime.yml
index f0a53ee7..7b52f11c 100644
--- a/meta/runtime.yml
+++ b/meta/runtime.yml
@@ -1,68 +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/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..75fc1ce5
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec module
+"""
+
+
+class Vpn_ipsecArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "ike_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "close_action": {
+ "type": "str",
+ "choices": ["none", "trap", "start"],
+ },
+ "dead_peer_detection": {
+ "type": "dict",
+ "options": {
+ "action": {
+ "type": "str",
+ "choices": ["trap", "clear", "restart"],
+ },
+ "interval": {"type": "int"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "disable_mobike": {"type": "bool"},
+ "ikev2_reauth": {"type": "bool"},
+ "key_exchange": {
+ "type": "str",
+ "choices": ["ikev1", "ikev2"],
+ },
+ "lifetime": {"type": "int"},
+ "mode": {"type": "str", "choices": ["main", "aggressive"]},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "dh_group": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ "prf": {"type": "str"},
+ },
+ },
+ },
+ },
+ "esp_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "compression": {"type": "bool"},
+ "disable_rekey": {"type": "bool"},
+ "life_bytes": {"type": "int"},
+ "life_packets": {"type": "int"},
+ "lifetime": {"type": "int"},
+ "mode": {
+ "type": "str",
+ "choices": ["tunnel", "transport"],
+ },
+ "pfs": {"type": "str"},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ },
+ },
+ },
+ },
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "psk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "dhcp_interface": {
+ "type": "list",
+ "elements": "str",
+ },
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ "ppk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ },
+ },
+ "profile": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "mode": {
+ "type": "str",
+ "choices": ["pre-shared-secret"],
+ },
+ "pre_shared_secret": {"type": "str", "no_log": True},
+ },
+ },
+ "bind_tunnel": {"type": "list", "elements": "str"},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "ike_group": {"type": "str"},
+ },
+ },
+ "interface": {"type": "list", "elements": "str"},
+ "log": {
+ "type": "dict",
+ "options": {
+ "level": {"type": "int"},
+ "subsystem": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "options": {
+ "type": "dict",
+ "options": {
+ "disable_route_autoinstall": {"type": "bool"},
+ "flexvpn": {"type": "bool"},
+ "interface": {"type": "str"},
+ "retransmission": {
+ "type": "dict",
+ "options": {
+ "attempts": {"type": "int"},
+ "base": {"type": "float"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "virtual_ip": {"type": "bool"},
+ },
+ },
+ "disable_uniqreqids": {"type": "bool"},
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..bd23d4d3
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec_s2s module
+"""
+
+
+class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec_s2s module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "peer": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "disable": {"type": "bool"},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "local_id": {"type": "str"},
+ "ppk": {
+ "type": "dict",
+ "options": {
+ "id": {"type": "str"},
+ "required": {"type": "bool"},
+ },
+ },
+ "rsa": {
+ "type": "dict",
+ "options": {
+ "local_key": {"type": "str", "no_log": True},
+ "passphrase": {"type": "str", "no_log": True},
+ "remote_key": {"type": "str", "no_log": True},
+ },
+ },
+ "x509": {
+ "type": "dict",
+ "options": {
+ "certificate": {"type": "str"},
+ "passphrase": {"type": "str", "no_log": True},
+ "ca_certificate": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "mode": {
+ "type": "str",
+ "choices": [
+ "pre-shared-secret",
+ "rsa",
+ "x509",
+ ],
+ },
+ "remote_id": {"type": "str"},
+ "use_x509_id": {"type": "bool"},
+ },
+ },
+ "childless": {
+ "type": "str",
+ "choices": ["allow", "prefer", "force", "never"],
+ },
+ "connection_type": {
+ "type": "str",
+ "choices": ["initiate", "trap", "none"],
+ },
+ "default_esp_group": {"type": "str"},
+ "description": {"type": "str"},
+ "dhcp_interface": {"type": "str"},
+ "force_udp_encapsulation": {"type": "bool"},
+ "ike_group": {"type": "str"},
+ "ikev2_reauth": {
+ "type": "str",
+ "choices": ["yes", "no", "inherit"],
+ },
+ "local_address": {"type": "str"},
+ "remote_address": {"type": "list", "elements": "str"},
+ "replay_window": {"type": "int"},
+ "tunnel": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "tunnel_id": {"type": "int", "required": True},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "local": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "protocol": {"type": "str"},
+ "priority": {"type": "int"},
+ "remote": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ "virtual_address": {"type": "list", "elements": "str"},
+ "vti": {
+ "type": "dict",
+ "options": {
+ "bind": {"type": "str"},
+ "esp_group": {"type": "str"},
+ "traffic_selector": {
+ "type": "dict",
+ "options": {
+ "local": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "remote": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..2d986cb4
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,665 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_vpn_ipsec config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+
+Follows the established per-module convention used by vyos_ha/vyos_nat
+(list-to-dict conversion + explicit per-state branching in
+generate_commands), rather than a shared generic engine.
+
+State semantics (standard Ansible RM convention, confirmed against a
+real device run that caught a bug in an earlier version of this file):
+ - merged: only items/fields named in `want` are touched. Nothing
+ absent from `want` is ever deleted.
+ - replaced: only items NAMED in `want` are touched (same item scope
+ as merged) -- but for each named item, its full state is
+ reconciled to exactly match `want` (fields present in
+ `have` but omitted from `want` ARE deleted). Items not
+ named in `want` at all are left completely alone.
+ - overridden: every item is in scope, including ones absent from
+ `want` entirely -- those get deleted wholesale. Named
+ items are reconciled the same way as `replaced`.
+
+This is implemented via two independent flags:
+ - select_all: whether item iteration considers have-only items too
+ (True only for overridden; False for merged/replaced).
+ - reconcile: whether omitted fields within an already-selected item
+ get deleted (True for replaced/overridden; False for
+ merged/rendered).
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
+ Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsec(ResourceModule):
+ """
+ The vyos_vpn_ipsec config class
+ """
+
+ def __init__(self, module):
+ super(Vpn_ipsec, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vpn_ipsec",
+ tmplt=Vpn_ipsecTemplate(),
+ )
+ self.parsers = [
+ "esp_group",
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ "esp_group.compression",
+ "esp_group.disable_rekey",
+ "esp_group.life_bytes",
+ "esp_group.life_packets",
+ "esp_group.lifetime",
+ "esp_group.mode",
+ "esp_group.pfs",
+ "ike_group",
+ "ike_group.key_exchange",
+ "ike_group.proposal",
+ "ike_group.proposal.dh_group",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.close_action",
+ "ike_group.dead_peer_detection.action",
+ "ike_group.dead_peer_detection.interval",
+ "ike_group.dead_peer_detection.timeout",
+ "ike_group.disable_mobike",
+ "ike_group.ikev2_reauth",
+ "ike_group.lifetime",
+ "ike_group.mode",
+ "profile",
+ "profile.authentication.mode",
+ "profile.authentication.pre_shared_secret",
+ "profile.esp_group",
+ "profile.ike_group",
+ "profile.disable",
+ "authentication.psk.secret_type",
+ "authentication.psk.dhcp_interface",
+ "authentication.ppk",
+ "authentication.ppk.id",
+ "authentication.ppk.secret",
+ "authentication.ppk.secret_type",
+ "interface",
+ "log.level",
+ "log.subsystem",
+ "options.disable_route_autoinstall",
+ "options.flexvpn",
+ "options.interface",
+ "options.retransmission.attempts",
+ "options.retransmission.base",
+ "options.retransmission.timeout",
+ "options.virtual_ip",
+ "disable_uniqreqids",
+ ]
+
+ def execute_module(self):
+ """Execute the module
+
+ :rtype: A dictionary
+ :returns: The result from module execution
+ """
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ """Generate configuration commands to send based on
+ want, have and desired state.
+ """
+ wantd = deepcopy(self.want) or {}
+ haved = deepcopy(self.have) or {}
+
+ for entry in (wantd, haved):
+ self._list_to_dict(entry)
+
+ scoped_delete = None
+ if self.state == "deleted":
+ if wantd:
+ # user named specific items -- surgical removal of just
+ # those, everything else preserved (vyos_vrf precedent:
+ # deleted + instances:[{name: vrf-blue}] removes only
+ # vrf-blue). Capture what was named before wiping wantd.
+ scoped_delete = wantd
+ wantd = {}
+
+ if self.state == "merged":
+ # NOTE: list_to_dict() above must run BEFORE this. dict_merge
+ # concatenates lists rather than merging matching entries by
+ # key, so merging while ike_group/esp_group/etc are still
+ # lists would duplicate entries instead of filling in omitted
+ # fields from `have`. Once they're name-keyed dicts, dict_merge
+ # recurses per-key correctly, which is what lets a partial
+ # update (e.g. specifying only key_exchange) leave other
+ # existing fields on that same group untouched.
+ wantd = dict_merge(haved, wantd)
+
+ select_all = self.state in ("overridden", "deleted")
+ reconcile = self.state in ("replaced", "overridden", "deleted")
+
+ self._compare_esp_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ike_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_profiles(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_psks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ppks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_top_level(wantd, haved, select_all, reconcile, scoped_delete)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------
+ # List -> name-keyed dict conversion (matches vyos_ha/vyos_nat style)
+ # -------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ for key in ("ike_group", "esp_group", "profile"):
+ items = config.get(key)
+ if isinstance(items, list):
+ config[key] = {item["name"]: item for item in items}
+ for item in config[key].values():
+ if isinstance(item.get("proposal"), list):
+ item["proposal"] = {p["proposal_id"]: p for p in item["proposal"]}
+
+ auth = config.get("authentication", {})
+ for key in ("psk", "ppk"):
+ items = auth.get(key)
+ if isinstance(items, list):
+ auth[key] = {item["name"]: item for item in items}
+
+ # -------------------------------------------------------------------
+ # ESP groups
+ # -------------------------------------------------------------------
+
+ def _compare_esp_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("esp_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("esp_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("esp_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ # only reached when select_all (overridden): item entirely
+ # absent from want -> delete wholesale
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "esp_group", False)
+
+ for field in ("mode", "pfs", "lifetime", "life_bytes", "life_packets"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("compression", "disable_rekey"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ None,
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # IKE groups
+ # -------------------------------------------------------------------
+
+ def _compare_ike_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("ike_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("ike_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("ike_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "ike_group", False)
+
+ self._cmp_scalar(
+ w,
+ h,
+ "key_exchange",
+ {"name": name},
+ "ike_group.key_exchange",
+ reconcile,
+ )
+ for field in ("close_action", "lifetime", "mode"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("disable_mobike", "ikev2_reauth"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+
+ w_dpd = w.get("dead_peer_detection", {})
+ h_dpd = h.get("dead_peer_detection", {})
+ for field in ("action", "interval", "timeout"):
+ self._cmp_scalar(
+ w_dpd,
+ h_dpd,
+ field,
+ {"name": name},
+ "ike_group.dead_peer_detection.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "ike_group.proposal",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.proposal.dh_group",
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Proposals (shared by esp_group / ike_group)
+ # -------------------------------------------------------------------
+
+ def _compare_proposals(
+ self,
+ want_props,
+ have_props,
+ group_ctx,
+ bare_parser,
+ encryption_parser,
+ hash_parser,
+ dh_group_parser,
+ reconcile,
+ ):
+ # a proposal collection lives entirely inside an already-selected
+ # group -- once that group is in scope, its own proposals always
+ # get full reconciliation under replaced/overridden (never a
+ # separate select_all concern of their own).
+ ids = set(want_props) | set(have_props) if reconcile else set(want_props)
+ for pid in ids:
+ w = want_props.get(pid, {})
+ h = have_props.get(pid, {})
+ if w == h:
+ continue
+
+ if pid in have_props and pid not in want_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, True)
+ continue
+
+ if pid not in have_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, False)
+
+ ctx = dict(group_ctx, proposal_id=pid)
+ self._cmp_scalar(w, h, "encryption", ctx, encryption_parser, reconcile)
+ self._cmp_scalar(w, h, "hash", ctx, hash_parser, reconcile)
+ if dh_group_parser:
+ self._cmp_scalar(w, h, "dh_group", ctx, dh_group_parser, reconcile)
+
+ # -------------------------------------------------------------------
+ # Profiles
+ # -------------------------------------------------------------------
+
+ def _compare_profiles(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_profiles = haved.get("profile", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("profile", {})):
+ if name in have_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ return
+
+ want_profiles = wantd.get("profile", {})
+ names = set(want_profiles) | set(have_profiles) if select_all else set(want_profiles)
+
+ for name in names:
+ w = want_profiles.get(name, {})
+ h = have_profiles.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_profiles and name not in want_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ continue
+
+ if name not in have_profiles:
+ self.addcmd({"name": name}, "profile", False)
+
+ ctx = {"name": name}
+ w_auth = w.get("authentication", {})
+ h_auth = h.get("authentication", {})
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "mode",
+ ctx,
+ "profile.authentication.mode",
+ reconcile,
+ )
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "pre_shared_secret",
+ ctx,
+ "profile.authentication.pre_shared_secret",
+ reconcile,
+ )
+ self._cmp_scalar(w, h, "esp_group", ctx, "profile.esp_group", reconcile)
+ self._cmp_scalar(w, h, "ike_group", ctx, "profile.ike_group", reconcile)
+ self._cmp_bool(w, h, "disable", ctx, "profile.disable", reconcile)
+
+ w_tunnels = set(w.get("bind_tunnel") or [])
+ h_tunnels = set(h.get("bind_tunnel") or [])
+ for tun in w_tunnels - h_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", False)
+ if reconcile:
+ for tun in h_tunnels - w_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", True)
+
+ # -------------------------------------------------------------------
+ # PSKs
+ # -------------------------------------------------------------------
+
+ def _compare_psks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_psks = haved.get("authentication", {}).get("psk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("psk", {})):
+ if name in have_psks:
+ self.commands.append(
+ "delete vpn ipsec authentication psk {0}".format(name),
+ )
+ return
+
+ want_psks = wantd.get("authentication", {}).get("psk", {})
+ names = set(want_psks) | set(have_psks) if select_all else set(want_psks)
+
+ for name in names:
+ w = want_psks.get(name, {})
+ h = have_psks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_psks and name not in want_psks:
+ self.commands.append("delete vpn ipsec authentication psk {0}".format(name))
+ continue
+
+ if name not in have_psks:
+ self.addcmd({"name": name}, "authentication.psk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.psk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.psk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", True)
+
+ w_dhcp = set(w.get("dhcp_interface") or [])
+ h_dhcp = set(h.get("dhcp_interface") or [])
+ for i in w_dhcp - h_dhcp:
+ self.addcmd(dict(ctx, dhcp_interface=i), "authentication.psk.dhcp_interface", False)
+ if reconcile:
+ for i in h_dhcp - w_dhcp:
+ self.addcmd(
+ dict(ctx, dhcp_interface=i),
+ "authentication.psk.dhcp_interface",
+ True,
+ )
+
+ def _compare_ppks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_ppks = haved.get("authentication", {}).get("ppk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("ppk", {})):
+ if name in have_ppks:
+ self.commands.append(
+ "delete vpn ipsec authentication ppk {0}".format(name),
+ )
+ return
+
+ want_ppks = wantd.get("authentication", {}).get("ppk", {})
+ names = set(want_ppks) | set(have_ppks) if select_all else set(want_ppks)
+
+ for name in names:
+ w = want_ppks.get(name, {})
+ h = have_ppks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_ppks and name not in want_ppks:
+ self.commands.append("delete vpn ipsec authentication ppk {0}".format(name))
+ continue
+
+ if name not in have_ppks:
+ self.addcmd({"name": name}, "authentication.ppk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.ppk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.ppk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", True)
+
+ # -------------------------------------------------------------------
+ # Top-level scalar / list / bool fields
+ #
+ # NOTE: these are all direct fields of the single top-level config
+ # object, not named collections -- there is no "item entirely absent
+ # from want" concept here, only "field omitted from want". So only
+ # `reconcile` applies; `select_all` is irrelevant at this level (it's
+ # accepted for a consistent call signature but unused).
+ # -------------------------------------------------------------------
+
+ def _compare_top_level(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ if scoped_delete is not None:
+ # Principle: naming a parameter under scoped `deleted` means
+ # "delete this specific value" -- a scalar/bool key present
+ # (regardless of value) signals whole-field removal; a list
+ # value present means "delete exactly these elements", not
+ # the whole list, mirroring vyos_vrf's bind_to_all precedent
+ # extended consistently to list- and nested-dict-shaped
+ # fields.
+ if "disable_uniqreqids" in scoped_delete and haved.get("disable_uniqreqids"):
+ self.commands.append("delete vpn ipsec disable-uniqreqids")
+
+ h_ifaces = set(haved.get("interface") or [])
+ for i in set(scoped_delete.get("interface") or []) & h_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ s_log = scoped_delete.get("log", {})
+ h_log = haved.get("log", {})
+ if "level" in s_log and "level" in h_log:
+ self.addcmd({"level": h_log["level"]}, "log.level", True)
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in set(s_log.get("subsystem") or []) & h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ s_opt = scoped_delete.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ if field in s_opt and h_opt.get(field):
+ self.addcmd({}, "options.{0}".format(field), True)
+ if "interface" in s_opt and "interface" in h_opt:
+ self.addcmd({"interface": h_opt["interface"]}, "options.interface", True)
+
+ s_retrans = s_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ if field in s_retrans and field in h_retrans:
+ self.addcmd(
+ {field: h_retrans[field]},
+ "options.retransmission.{0}".format(field),
+ True,
+ )
+ return
+
+ self._cmp_bool(wantd, haved, "disable_uniqreqids", {}, "disable_uniqreqids", reconcile)
+
+ w_ifaces = set(wantd.get("interface") or [])
+ h_ifaces = set(haved.get("interface") or [])
+ for i in w_ifaces - h_ifaces:
+ self.addcmd({"interface": i}, "interface", False)
+ if reconcile:
+ for i in h_ifaces - w_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ w_log = wantd.get("log", {})
+ h_log = haved.get("log", {})
+ self._cmp_scalar(w_log, h_log, "level", {}, "log.level", reconcile)
+ w_sub = set(w_log.get("subsystem") or [])
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in w_sub - h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", False)
+ if reconcile:
+ for s in h_sub - w_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ w_opt = wantd.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ self._cmp_bool(w_opt, h_opt, field, {}, "options.{0}".format(field), reconcile)
+ self._cmp_scalar(w_opt, h_opt, "interface", {}, "options.interface", reconcile)
+
+ w_retrans = w_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ self._cmp_scalar(
+ w_retrans,
+ h_retrans,
+ field,
+ {},
+ "options.retransmission.{0}".format(field),
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Field-level helpers (mirrors vyos_nat's _cmp_scalar / _cmp_bool)
+ # -------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif reconcile and h is not None:
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False):
+ # An explicitly-specified value (even False) is always enforced,
+ # regardless of state -- that's the user directly saying what
+ # they want. An OMITTED field is only enforced (i.e. deleted if
+ # currently True) under full reconciliation (replaced/overridden).
+ # Under merged, an omitted field is left alone -- protected
+ # further upstream by dict_merge backfilling `want` from `have`
+ # before this is ever reached, but this still needs to be correct
+ # in isolation (e.g. for a field nested inside a dict that wasn't
+ # part of the dict_merge'd top-level structure).
+ explicit = field in want
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h and (w or explicit or reconcile):
+ self.addcmd(dict(ctx), parser, not w)
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
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 24c444dd..92eaa327 100644
--- a/plugins/module_utils/network/vyos/facts/facts.py
+++ b/plugins/module_utils/network/vyos/facts/facts.py
@@ -1,133 +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/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..695b9b8d
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha (explicit process_facts() naming each
+name-keyed dict that needs converting to a list), matching the config.py
+convention for this module, rather than a generic argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsecFacts(object):
+ """The vyos vpn_ipsec facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsecArgs.argument_spec
+
+ def get_vpn_ipsec_data(self, connection):
+ return connection.get('show configuration commands | match "vpn ipsec"')
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects. Each key handled explicitly, matching
+ the vyos_logging_global/vyos_ha convention.
+ """
+ if not objFinal:
+ return objFinal
+
+ for key in ("ike_group", "esp_group"):
+ if key in objFinal:
+ items = list(objFinal[key].values())
+ for item in items:
+ if "proposal" in item:
+ item["proposal"] = sorted(
+ item["proposal"].values(),
+ key=lambda p: int(p["proposal_id"]),
+ )
+ objFinal[key] = sorted(items, key=lambda item: item["name"])
+
+ if "profile" in objFinal:
+ objFinal["profile"] = sorted(
+ objFinal["profile"].values(),
+ key=lambda item: item["name"],
+ )
+
+ if "authentication" in objFinal:
+ auth = objFinal["authentication"]
+ for key in ("psk", "ppk"):
+ if key in auth:
+ auth[key] = sorted(
+ auth[key].values(),
+ key=lambda item: item["name"],
+ )
+
+ return objFinal
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_data(connection)
+
+ vpn_ipsec_parser = Vpn_ipsecTemplate(lines=data.splitlines(), module=self._module)
+ objs = vpn_ipsec_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..6c9b5d8e
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,115 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec_s2s fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts()
+naming each name-keyed dict that needs converting to a list), matching
+the config.py convention for this module, rather than a generic
+argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sTemplate,
+)
+
+
+class Vpn_ipsec_s2sFacts(object):
+ """The vyos vpn_ipsec_s2s facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec
+
+ def get_vpn_ipsec_s2s_data(self, connection):
+ return connection.get(
+ 'show configuration commands | match "vpn ipsec site-to-site"',
+ )
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects.
+
+ NOTE: every PARSERS result template in rm_templates.py nests its
+ output under "site_to_site" -> "peer" (mirroring the CLI's own
+ tree: `vpn ipsec site-to-site peer <name> ...`), but the
+ argspec's `config` has `peer` directly at the top level -- there
+ is no `site_to_site` wrapper in the argspec, since that's the
+ one node wrap_docstring.py unwrapped when building the
+ docstring (its own children became config's children directly).
+ So this needs to strip that outer key, not just convert the
+ name-keyed dicts to lists.
+ """
+ if not objFinal:
+ return objFinal
+
+ site_to_site = objFinal.get("site_to_site", {})
+ peers = site_to_site.get("peer", {})
+
+ items = list(peers.values())
+ for item in items:
+ if "tunnel" in item:
+ item["tunnel"] = sorted(
+ item["tunnel"].values(),
+ key=lambda t: int(t["tunnel_id"]),
+ )
+
+ return {"peer": sorted(items, key=lambda item: item["name"])}
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec_s2s network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_s2s_data(connection)
+
+ vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate(
+ lines=data.splitlines(),
+ module=self._module,
+ )
+ objs = vpn_ipsec_s2s_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_s2s_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec_s2s"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py
new file mode 100644
index 00000000..ff889526
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py
@@ -0,0 +1,997 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The VPN IPSEC parser templates file. This contains
+a list of parser definitions and associated functions that
+facilitates both facts gathering and native command generation for
+the given network resource.
+"""
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+class Vpn_ipsecTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Vpn_ipsecTemplate, self).__init__(
+ lines=lines,
+ tmplt=self,
+ prefix=prefix,
+ module=module,
+ )
+
+ # fmt: off
+ PARSERS = [
+ # ---------------------------------------------------------------
+ # esp-group
+ # ---------------------------------------------------------------
+ {
+ "name": "esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal.encryption",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sencryption\s'?(?P<encryption>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "encryption": "{{ encryption }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.proposal.hash",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \shash\s'?(?P<hash>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "hash": "{{ hash }}",
+ },
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # ike-group
+ # ---------------------------------------------------------------
+ {
+ "name": "ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.key_exchange",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \skey-exchange\s'?(?P<key_exchange>\w+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} key-exchange {{ key_exchange }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "key_exchange": "{{ key_exchange }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.dh_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sdh-group\s'?(?P<dh_group>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} dh-group {{ dh_group }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "dh_group": "{{ dh_group }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.encryption",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \sencryption\s'?(?P<encryption>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} encryption {{ encryption }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "encryption": "{{ encryption }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.proposal.hash",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sproposal\s(?P<proposal_id>\d+)
+ \shash\s'?(?P<hash>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} proposal {{ proposal_id }} hash {{ hash }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "proposal": {
+ "{{ proposal_id }}": {
+ "proposal_id": "{{ proposal_id }}",
+ "hash": "{{ hash }}",
+ },
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication psk
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.psk",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \sid\s'?(?P<id>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} id {{ id }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "id": ["{{ id }}"],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \ssecret\s'?(?P<secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} secret '{{ secret }}'",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "secret": "{{ secret }}",
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # profile
+ # ---------------------------------------------------------------
+ {
+ "name": "profile",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.authentication.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sauthentication\smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} authentication mode {{ mode }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "authentication": {
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.authentication.pre_shared_secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sauthentication\spre-shared-secret\s'?(?P<pre_shared_secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} authentication pre-shared-secret '{{ pre_shared_secret }}'",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "authentication": {
+ "pre_shared_secret": "{{ pre_shared_secret }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.bind_tunnel",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sbind\stunnel\s'?(?P<bind_tunnel>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} bind tunnel {{ bind_tunnel }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "bind_tunnel": ["{{ bind_tunnel }}"],
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sesp-group\s'?(?P<esp_group>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} esp-group {{ esp_group }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "esp_group": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "profile.ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sike-group\s'?(?P<ike_group>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} ike-group {{ ike_group }}",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "ike_group": "{{ ike_group }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # ike-group: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "ike_group.close_action",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sclose-action\s'?(?P<close_action>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} close-action {{ close_action }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "close_action": "{{ close_action }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.action",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\saction\s'?(?P<action>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection action {{ action }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"action": "{{ action }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.interval",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\sinterval\s'?(?P<interval>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection interval {{ interval }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"interval": "{{ interval }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.dead_peer_detection.timeout",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdead-peer-detection\stimeout\s'?(?P<timeout>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} dead-peer-detection timeout {{ timeout }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "dead_peer_detection": {"timeout": "{{ timeout }}"},
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.disable_mobike",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sdisable-mobike
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} disable-mobike",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "disable_mobike": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.ikev2_reauth",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \sikev2-reauth
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} ikev2-reauth",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "ikev2_reauth": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.lifetime",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \slifetime\s'?(?P<lifetime>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} lifetime {{ lifetime }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "lifetime": "{{ lifetime }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "ike_group.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sike-group\s(?P<ike_group>\S+)
+ \smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec ike-group {{ name }} mode {{ mode }}",
+ "result": {
+ "ike_group": {
+ "{{ ike_group }}": {
+ "name": "{{ ike_group }}",
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # esp-group: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "esp_group.compression",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \scompression
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} compression",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "compression": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.disable_rekey",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \sdisable-rekey
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} disable-rekey",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "disable_rekey": True,
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.life_bytes",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slife-bytes\s'?(?P<life_bytes>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} life-bytes {{ life_bytes }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "life_bytes": "{{ life_bytes }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.life_packets",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slife-packets\s'?(?P<life_packets>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} life-packets {{ life_packets }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "life_packets": "{{ life_packets }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.lifetime",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \slifetime\s'?(?P<lifetime>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} lifetime {{ lifetime }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "lifetime": "{{ lifetime }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \smode\s'?(?P<mode>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} mode {{ mode }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "mode": "{{ mode }}",
+ },
+ },
+ },
+ },
+ {
+ "name": "esp_group.pfs",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sesp-group\s(?P<esp_group>\S+)
+ \spfs\s'?(?P<pfs>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec esp-group {{ name }} pfs {{ pfs }}",
+ "result": {
+ "esp_group": {
+ "{{ esp_group }}": {
+ "name": "{{ esp_group }}",
+ "pfs": "{{ pfs }}",
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication.psk: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.psk.secret_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \ssecret-type\s'?(?P<secret_type>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} secret-type {{ secret_type }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "secret_type": "{{ secret_type }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.psk.dhcp_interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\spsk\s(?P<psk>\S+)
+ \sdhcp-interface\s'?(?P<dhcp_interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication psk {{ name }} dhcp-interface {{ dhcp_interface }}",
+ "result": {
+ "authentication": {
+ "psk": {
+ "{{ psk }}": {
+ "name": "{{ psk }}",
+ "dhcp_interface": ["{{ dhcp_interface }}"],
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # authentication.ppk
+ # ---------------------------------------------------------------
+ {
+ "name": "authentication.ppk",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \sid\s'?(?P<id>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} id {{ id }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "id": ["{{ id }}"],
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.secret",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \ssecret\s'?(?P<secret>[^']+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} secret '{{ secret }}'",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "secret": "{{ secret }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "authentication.ppk.secret_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sauthentication\sppk\s(?P<ppk>\S+)
+ \ssecret-type\s'?(?P<secret_type>[\w-]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec authentication ppk {{ name }} secret-type {{ secret_type }}",
+ "result": {
+ "authentication": {
+ "ppk": {
+ "{{ ppk }}": {
+ "name": "{{ ppk }}",
+ "secret_type": "{{ secret_type }}",
+ },
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # profile: remaining fields
+ # ---------------------------------------------------------------
+ {
+ "name": "profile.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sprofile\s(?P<profile>\S+)
+ \sdisable
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec profile {{ name }} disable",
+ "result": {
+ "profile": {
+ "{{ profile }}": {
+ "name": "{{ profile }}",
+ "disable": True,
+ },
+ },
+ },
+ },
+
+ # ---------------------------------------------------------------
+ # top-level: interface, log, options, disable_uniqreqids
+ # ---------------------------------------------------------------
+ {
+ "name": "interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sinterface\s'?(?P<interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec interface {{ interface }}",
+ "result": {
+ "interface": ["{{ interface }}"],
+ },
+ },
+ {
+ "name": "log.level",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\slog\slevel\s'?(?P<level>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec log level {{ level }}",
+ "result": {
+ "log": {"level": "{{ level }}"},
+ },
+ },
+ {
+ "name": "log.subsystem",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\slog\ssubsystem\s'?(?P<subsystem>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec log subsystem {{ subsystem }}",
+ "result": {
+ "log": {"subsystem": ["{{ subsystem }}"]},
+ },
+ },
+ {
+ "name": "options.disable_route_autoinstall",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sdisable-route-autoinstall
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options disable-route-autoinstall",
+ "result": {
+ "options": {"disable_route_autoinstall": True},
+ },
+ },
+ {
+ "name": "options.flexvpn",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sflexvpn
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options flexvpn",
+ "result": {
+ "options": {"flexvpn": True},
+ },
+ },
+ {
+ "name": "options.interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sinterface\s'?(?P<interface>\S+?)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options interface {{ interface }}",
+ "result": {
+ "options": {"interface": "{{ interface }}"},
+ },
+ },
+ {
+ "name": "options.retransmission.attempts",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\sattempts\s'?(?P<attempts>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission attempts {{ attempts }}",
+ "result": {
+ "options": {"retransmission": {"attempts": "{{ attempts }}"}},
+ },
+ },
+ {
+ "name": "options.retransmission.base",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\sbase\s'?(?P<base>[\d.]+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission base {{ base }}",
+ "result": {
+ "options": {"retransmission": {"base": "{{ base }}"}},
+ },
+ },
+ {
+ "name": "options.retransmission.timeout",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\sretransmission\stimeout\s'?(?P<timeout>\d+)'?
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options retransmission timeout {{ timeout }}",
+ "result": {
+ "options": {"retransmission": {"timeout": "{{ timeout }}"}},
+ },
+ },
+ {
+ "name": "options.virtual_ip",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\soptions\svirtual-ip
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec options virtual-ip",
+ "result": {
+ "options": {"virtual_ip": True},
+ },
+ },
+ {
+ "name": "disable_uniqreqids",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\sdisable-uniqreqids
+ \s*$""", re.VERBOSE,
+ ),
+ "setval": "vpn ipsec disable-uniqreqids",
+ "result": {
+ "disable_uniqreqids": True,
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..4319019c
--- /dev/null
+++ b/plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py
@@ -0,0 +1,670 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import re
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.network_template import (
+ NetworkTemplate,
+)
+
+
+class Vpn_ipsec_s2sTemplate(NetworkTemplate):
+ def __init__(self, lines=None, module=None):
+ prefix = {"set": "set", "remove": "delete"}
+ super(Vpn_ipsec_s2sTemplate, self).__init__(
+ lines=lines,
+ tmplt=self,
+ module=module,
+ prefix=prefix,
+ )
+
+ # fmt: off
+ PARSERS = [
+ {
+ "name": "peer",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }}",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)\sdisable$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} disable",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "disable": True}}},
+ },
+ },
+ {
+ "name": "peer.authentication.local_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\slocal-id\s'(?P<local_id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication local-id '{{ local_id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"local_id": "{{ local_id }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.remote_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sremote-id\s'(?P<remote_id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication remote-id '{{ remote_id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"remote_id": "{{ remote_id }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.mode",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\smode\s'(?P<mode>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication mode '{{ mode }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"mode": "{{ mode }}"}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.use_x509_id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\suse-x509-id$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication use-x509-id",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"use_x509_id": True}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.ppk.id",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sppk\sid\s'(?P<id>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk id '{{ id }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"id": "{{ id }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.ppk.required",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sppk\srequired$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication ppk required",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"ppk": {"required": True}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.local_key",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\slocal-key\s'(?P<local_key>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa local-key '{{ local_key }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"local_key": "{{ local_key }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.remote_key",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\sremote-key\s'(?P<remote_key>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa remote-key '{{ remote_key }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"remote_key": "{{ remote_key }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.rsa.passphrase",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\srsa\spassphrase\s'(?P<passphrase>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication rsa passphrase '{{ passphrase }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"rsa": {"passphrase": "{{ passphrase }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.certificate",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\scertificate\s'(?P<certificate>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 certificate '{{ certificate }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"certificate": "{{ certificate }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.passphrase",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\spassphrase\s'(?P<passphrase>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 passphrase '{{ passphrase }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"passphrase": "{{ passphrase }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.authentication.x509.ca_certificate",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sauthentication\sx509\sca-certificate\s'(?P<ca_certificate>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} authentication x509 ca-certificate '{{ ca_certificate }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "authentication": {"x509": {"ca_certificate": ["{{ ca_certificate }}"]}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.childless",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \schildless\s'(?P<childless>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} childless '{{ childless }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "childless": "{{ childless }}"}}},
+ },
+ },
+ {
+ "name": "peer.connection_type",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sconnection-type\s'(?P<connection_type>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} connection-type '{{ connection_type }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "connection_type": "{{ connection_type }}"}}},
+ },
+ },
+ {
+ "name": "peer.default_esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdefault-esp-group\s'(?P<default_esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} default-esp-group '{{ default_esp_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "default_esp_group": "{{ default_esp_group }}"}}},
+ },
+ },
+ {
+ "name": "peer.description",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdescription\s'(?P<description>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} description '{{ description }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "description": "{{ description }}"}}},
+ },
+ },
+ {
+ "name": "peer.dhcp_interface",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sdhcp-interface\s'(?P<dhcp_interface>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} dhcp-interface '{{ dhcp_interface }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "dhcp_interface": "{{ dhcp_interface }}"}}},
+ },
+ },
+ {
+ "name": "peer.force_udp_encapsulation",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sforce-udp-encapsulation$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} force-udp-encapsulation",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "force_udp_encapsulation": True}}},
+ },
+ },
+ {
+ "name": "peer.ike_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sike-group\s'(?P<ike_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} ike-group '{{ ike_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ike_group": "{{ ike_group }}"}}},
+ },
+ },
+ {
+ "name": "peer.ikev2_reauth",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sikev2-reauth\s'(?P<ikev2_reauth>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} ikev2-reauth '{{ ikev2_reauth }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "ikev2_reauth": "{{ ikev2_reauth }}"}}},
+ },
+ },
+ {
+ "name": "peer.local_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \slocal-address\s'(?P<local_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} local-address '{{ local_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "local_address": "{{ local_address }}"}}},
+ },
+ },
+ {
+ "name": "peer.remote_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sremote-address\s'(?P<remote_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} remote-address '{{ remote_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "remote_address": ["{{ remote_address }}"]}}},
+ },
+ },
+ {
+ "name": "peer.replay_window",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \sreplay-window\s'(?P<replay_window>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} replay-window '{{ replay_window }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "replay_window": "{{ replay_window }}"}}},
+ },
+ },
+ {
+ "name": "peer.virtual_address",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svirtual-address\s'(?P<virtual_address>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} virtual-address '{{ virtual_address }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "virtual_address": ["{{ virtual_address }}"]}}},
+ },
+ },
+ {
+ "name": "peer.tunnel",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }}",
+ "result": {
+ "site_to_site": {
+ "peer": {"{{ name }}": {"name": "{{ name }}", "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}"}}}},
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.disable",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sdisable$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} disable",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "disable": True,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sesp-group\s'(?P<esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} esp-group '{{ esp_group }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "esp_group": "{{ esp_group }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.protocol",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sprotocol\s'(?P<protocol>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} protocol '{{ protocol }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "protocol": "{{ protocol }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.priority",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\spriority\s'(?P<priority>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} priority '{{ priority }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "priority": "{{ priority }}",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.local.port",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\slocal\sport\s'(?P<port>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local port '{{ port }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "local": {"port": "{{ port }}"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.local.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\slocal\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} local prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "local": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.remote.port",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sremote\sport\s'(?P<port>\d+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote port '{{ port }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {
+ "{{ tunnel_id }}": {
+ "tunnel_id": "{{ tunnel_id }}",
+ "remote": {"port": "{{ port }}"},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.tunnel.remote.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \stunnel\s(?P<tunnel_id>\d+)\sremote\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} tunnel {{ tunnel_id }} remote prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "tunnel": {"{{ tunnel_id }}": {"tunnel_id": "{{ tunnel_id }}", "remote": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.vti.bind",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\sbind\s'(?P<bind>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti bind '{{ bind }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"bind": "{{ bind }}"}}}},
+ },
+ },
+ {
+ "name": "peer.vti.esp_group",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\sesp-group\s'(?P<esp_group>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti esp-group '{{ esp_group }}'",
+ "result": {
+ "site_to_site": {"peer": {"{{ name }}": {"name": "{{ name }}", "vti": {"esp_group": "{{ esp_group }}"}}}},
+ },
+ },
+ {
+ "name": "peer.vti.traffic_selector.local.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\straffic-selector\slocal\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector local prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "vti": {"traffic_selector": {"local": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ {
+ "name": "peer.vti.traffic_selector.remote.prefix",
+ "getval": re.compile(
+ r"""
+ ^set\svpn\sipsec\ssite-to-site\speer\s(?P<name>\S+)
+ \svti\straffic-selector\sremote\sprefix\s'(?P<prefix>[^']+)'$
+ """, re.VERBOSE,
+ ),
+ "setval": "vpn ipsec site-to-site peer {{ name }} vti traffic-selector remote prefix '{{ prefix }}'",
+ "result": {
+ "site_to_site": {
+ "peer": {
+ "{{ name }}": {
+ "name": "{{ name }}",
+ "vti": {"traffic_selector": {"remote": {"prefix": ["{{ prefix }}"]}}},
+ },
+ },
+ },
+ },
+ },
+ ]
+ # fmt: on
diff --git a/plugins/modules/vyos_vpn_ipsec.py b/plugins/modules/vyos_vpn_ipsec.py
new file mode 100644
index 00000000..9af12ff7
--- /dev/null
+++ b/plugins/modules/vyos_vpn_ipsec.py
@@ -0,0 +1,454 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+"""
+The module file for vyos_vpn_ipsec
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = """
+module: vyos_vpn_ipsec
+short_description: Manages global IPsec (ike-group, esp-group, profile, authentication, options) attributes of VyOS network devices.
+description: This module manages global VPN IPsec configuration on VyOS devices
+ -- IKE groups, ESP groups, PSK/PPK authentication, IPsec profiles, and global
+ options. Site-to-site peers and IKEv2 remote-access connections are handled by
+ separate modules.
+version_added: 1.0.0
+author: Evgeny Molotkov (@omnom62)
+extends_documentation_fragment:
+ - vyos.vyos.vyos
+notes:
+ - Tested against VyOS 1.4 and 1.5.
+ - "Source of truth for field types/choices: device node.def templates under /opt/vyatta/share/vyatta-cfg/templates/vpn/ipsec/."
+options:
+ config:
+ description: IPsec global configuration.
+ type: dict
+ suboptions:
+ ike_group:
+ description: List of IKE groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: The name of the IKE group.
+ type: str
+ required: true
+ close_action:
+ description: Action to take if a child SA is unexpectedly closed.
+ type: str
+ choices: [none, trap, start]
+ dead_peer_detection:
+ description: Dead Peer Detection (DPD).
+ type: dict
+ suboptions:
+ action:
+ description: Keep-alive failure action.
+ type: str
+ choices: [trap, clear, restart]
+ interval:
+ description: Keep-alive interval in seconds.
+ type: int
+ timeout:
+ description: Dead Peer Detection keep-alive timeout (IKEv1 only), in seconds.
+ type: int
+ disable_mobike:
+ description: Disable MOBIKE support (IKEv2 only).
+ type: bool
+ ikev2_reauth:
+ description: Re-authentication of the remote peer during an IKE re-key (IKEv2 only).
+ type: bool
+ key_exchange:
+ description: IKE version.
+ type: str
+ choices: [ikev1, ikev2]
+ lifetime:
+ description: IKE lifetime in seconds.
+ type: int
+ mode:
+ description: IKEv1 phase 1 mode.
+ type: str
+ choices: [main, aggressive]
+ proposal:
+ description: List of IKE proposals.
+ type: list
+ elements: dict
+ suboptions:
+ proposal_id:
+ description: The proposal identifier.
+ type: int
+ dh_group:
+ description: Diffie-Hellman group. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side, not enumerated here since
+ the set is version-dependent.
+ type: int
+ encryption:
+ description: Encryption algorithm. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side, not enumerated here since
+ the set is version-dependent.
+ type: str
+ hash:
+ description: Hash algorithm. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side.
+ type: str
+ prf:
+ description: Pseudo-Random Function. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side.
+ type: str
+ esp_group:
+ description: List of ESP groups.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: The name of the ESP group.
+ type: str
+ required: true
+ compression:
+ description: Enable ESP compression.
+ type: bool
+ disable_rekey:
+ description: Do not locally initiate a re-key of the SA; remote peer must re-key before expiration.
+ type: bool
+ life_bytes:
+ description: Security Association byte count to expire.
+ type: int
+ life_packets:
+ description: Security Association packet count to expire.
+ type: int
+ lifetime:
+ description: Security Association time to expire, in seconds.
+ type: int
+ mode:
+ description: ESP mode.
+ type: str
+ choices: [tunnel, transport]
+ pfs:
+ description: ESP Perfect Forward Secrecy. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side, not enumerated here since
+ the set is version-dependent.
+ type: str
+ proposal:
+ description: List of ESP proposals.
+ type: list
+ elements: dict
+ suboptions:
+ proposal_id:
+ description: The proposal identifier.
+ type: int
+ encryption:
+ description: Encryption algorithm. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side, not enumerated here since
+ the set is version-dependent.
+ type: str
+ hash:
+ description: Hash algorithm. See VyOS/strongSwan documentation for the
+ full set of valid values -- validated device-side.
+ type: str
+ authentication:
+ description: Global pre-shared-key and post-quantum pre-shared-key definitions.
+ type: dict
+ suboptions:
+ psk:
+ description: List of pre-shared keys.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Pre-shared key name.
+ type: str
+ required: true
+ id:
+ description: ID(s) for authentication.
+ type: list
+ elements: str
+ dhcp_interface:
+ description: DHCP interface(s) supplying next-hop IP address.
+ type: list
+ elements: str
+ secret:
+ description: IKE pre-shared secret key.
+ type: str
+ secret_type:
+ description: Secret encoding type.
+ type: str
+ choices: [base64, hex, plaintext]
+ ppk:
+ description: List of post-quantum pre-shared keys.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Post-quantum pre-shared key name.
+ type: str
+ required: true
+ id:
+ description: ID(s) for PPK.
+ type: list
+ elements: str
+ secret:
+ description: Post-quantum pre-shared secret key.
+ type: str
+ secret_type:
+ description: Secret encoding type.
+ type: str
+ choices: [base64, hex, plaintext]
+ profile:
+ description: List of VPN IPsec profiles (used for e.g. DMVPN/GRE tunnel binding).
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Profile name.
+ type: str
+ required: true
+ authentication:
+ description: Authentication settings for this profile.
+ type: dict
+ suboptions:
+ mode:
+ description: Authentication mode.
+ type: str
+ choices: [pre-shared-secret]
+ pre_shared_secret:
+ description: Pre-shared secret key.
+ type: str
+ bind_tunnel:
+ description: Tunnel interface(s) associated with this profile.
+ type: list
+ elements: str
+ disable:
+ description: Disable this profile.
+ type: bool
+ esp_group:
+ description: ESP group name to use for this profile.
+ type: str
+ ike_group:
+ description: IKE group name to use for this profile.
+ type: str
+ interface:
+ description: Interface(s) IPsec listens on. If omitted, listens on all interfaces.
+ type: list
+ elements: str
+ log:
+ description: IPsec logging settings.
+ type: dict
+ suboptions:
+ level:
+ description: Global IPsec logging level.
+ type: int
+ subsystem:
+ description: Per-subsystem logging levels to enable.
+ type: list
+ elements: str
+ options:
+ description: Global IPsec options.
+ type: dict
+ suboptions:
+ disable_route_autoinstall:
+ description: Do not automatically install routes to remote networks.
+ type: bool
+ flexvpn:
+ description: Allow FlexVPN vendor ID payload (IKEv2 only).
+ type: bool
+ interface:
+ description: Single interface for IPsec options scope (distinct from top-level interface list).
+ type: str
+ retransmission:
+ description: IPsec retransmission settings.
+ type: dict
+ suboptions:
+ attempts:
+ description: Maximum number of retransmissions.
+ type: int
+ base:
+ description: Base of exponential backoff.
+ type: float
+ timeout:
+ description: Timeout in seconds before the first retransmission.
+ type: int
+ virtual_ip:
+ description: Allow install of virtual-ip addresses.
+ type: bool
+ disable_uniqreqids:
+ description: Disable requirement for unique IDs in the Security Database.
+ type: bool
+ 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").
+ - The states I(replaced) and I(overridden) have identical behaviour for this module
+ with respect to named collections (ike_group, esp_group, profile, authentication),
+ but differ in scope -- see the module description for detail.
+ - 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 provided configuration with device configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-TEST
+ key_exchange: ikev2
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ dh_group: 14
+ state: merged
+
+- name: Replace one named esp-group, leaving all other groups untouched
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+ state: replaced
+
+- name: Override the whole configuration -- anything not listed here is removed
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ state: overridden
+
+- name: Delete one named esp-group, leaving all other groups untouched
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ state: deleted
+
+- name: Remove all vpn_ipsec configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ state: deleted
+
+- name: Gather current vpn_ipsec configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ state: gathered
+
+- name: Render configuration without touching the device
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ state: rendered
+
+- name: Parse raw config text into structured facts
+ vyos.vyos.vyos_vpn_ipsec:
+ running_config: "{{ lookup('file', './vpn_ipsec.cfg') }}"
+ state: parsed
+"""
+
+RETURN = """
+before:
+ description: The configuration prior to the module execution.
+ returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+after:
+ description: The resulting configuration after module execution.
+ returned: when changed
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+commands:
+ description: The set of commands pushed to the remote device.
+ returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ type: list
+ sample:
+ - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256
+ - set vpn ipsec ike-group IKE-TEST key-exchange ikev2
+rendered:
+ description: The provided configuration in the task rendered in device-native format (offline).
+ returned: when I(state) is C(rendered)
+ type: list
+ sample:
+ - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256
+gathered:
+ description: Facts about the network resource gathered from the remote device as structured data.
+ returned: when I(state) is C(gathered)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+parsed:
+ description: The device native config provided in I(running_config) option parsed into structured data as per module argspec.
+ returned: when I(state) is C(parsed)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.config.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsec,
+)
+
+
+def main():
+ """
+ Main entry point for module execution
+
+ :returns: the result form module invocation
+ """
+ module = AnsibleModule(
+ argument_spec=Vpn_ipsecArgs.argument_spec,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ["state", "merged", ["config"]],
+ ["state", "replaced", ["config"]],
+ ["state", "overridden", ["config"]],
+ ["state", "rendered", ["config"]],
+ ["state", "parsed", ["running_config"]],
+ ],
+ supports_check_mode=True,
+ )
+
+ result = Vpn_ipsec(module).execute_module()
+ module.exit_json(**result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_vpn_ipsec_s2s.py b/plugins/modules/vyos_vpn_ipsec_s2s.py
new file mode 100644
index 00000000..7458381e
--- /dev/null
+++ b/plugins/modules/vyos_vpn_ipsec_s2s.py
@@ -0,0 +1,337 @@
+#!/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: 1.0.0
+author: Evgeny Molotkov (@omnom62)
+extends_documentation_fragment:
+ - vyos.vyos.vyos
+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."
+ - "The argspec only requires I(name) on a peer, but VyOS itself enforces
+ several more requirements at commit time -- confirmed via real device
+ testing, not visible in the argspec: every peer needs C(authentication),
+ a real C(remote_address) (not just omitted), a C(local_address) or
+ C(dhcp_interface), and at least one of C(tunnel) or C(vti). A peer
+ missing any of these will pass Ansible's own argument validation but
+ fail the device commit with a specific error naming what's missing."
+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
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+after:
+ description: The resulting configuration after module execution.
+ returned: when changed
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+commands:
+ description: The set of commands pushed to the remote device.
+ returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ type: list
+ sample:
+ - set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST'
+ - set vpn ipsec site-to-site peer PEER-TEST default-esp-group 'ESP-TEST'
+rendered:
+ description: The provided configuration in the task rendered in device-native format (offline).
+ returned: when I(state) is C(rendered)
+ type: list
+ sample:
+ - set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST'
+gathered:
+ description: Facts about the network resource gathered from the remote device as structured data.
+ returned: when I(state) is C(gathered)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+parsed:
+ description: The device native config provided in I(running_config) option parsed into structured data as per module argspec.
+ returned: when I(state) is C(parsed)
+ type: dict
+ sample: >
+ This output will always be in the same format as the
+ module argspec.
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_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/defaults/main.yaml b/tests/integration/targets/vyos_vpn_ipsec/defaults/main.yaml
new file mode 100644
index 00000000..164afead
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/defaults/main.yaml
@@ -0,0 +1,3 @@
+---
+testcase: "[^_].*"
+test_items: []
diff --git a/tests/integration/targets/vyos_vpn_ipsec/meta/main.yaml b/tests/integration/targets/vyos_vpn_ipsec/meta/main.yaml
new file mode 100644
index 00000000..ed97d539
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/meta/main.yaml
@@ -0,0 +1 @@
+---
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tasks/cli.yaml b/tests/integration/targets/vyos_vpn_ipsec/tasks/cli.yaml
new file mode 100644
index 00000000..daccf720
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/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/tasks/main.yaml b/tests/integration/targets/vyos_vpn_ipsec/tasks/main.yaml
new file mode 100644
index 00000000..e6378581
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/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/tests/cli/_parsed.cfg b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_parsed.cfg
new file mode 100644
index 00000000..85a382e7
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_parsed.cfg
@@ -0,0 +1,3 @@
+set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 encryption 'aes256'
+set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 hash 'sha256'
+set vpn ipsec ike-group IKE-PARSE-TEST key-exchange 'ikev2'
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_populate.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_populate.yaml
new file mode 100644
index 00000000..96842807
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_populate.yaml
@@ -0,0 +1,8 @@
+---
+- ansible.builtin.include_tasks: _remove_config.yaml
+
+- name: POPULATE Apply the provided configuration
+ register: result
+ vyos.vyos.vyos_vpn_ipsec:
+ config: "{{ populate_config }}"
+ state: merged
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_remove_config.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_remove_config.yaml
new file mode 100644
index 00000000..14b032e3
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/_remove_config.yaml
@@ -0,0 +1,4 @@
+---
+- name: delete the provided configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ state: deleted
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tests/cli/deleted.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/deleted.yaml
new file mode 100644
index 00000000..eee2c822
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/deleted.yaml
@@ -0,0 +1,69 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec deleted integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: TEST - [deleted] Remove all configuration
+ vyos.vyos.vyos_vpn_ipsec:
+ 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:
+ 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 IKE-EXTRA by name
+ vyos.vyos.vyos_vpn_ipsec:
+ config:
+ ike_group:
+ - name: IKE-EXTRA
+ state: deleted
+ register: result
+
+ - name: TEST - [deleted scoped] Assert only IKE-EXTRA was removed
+ assert:
+ that:
+ - result.commands == ["delete vpn ipsec ike-group IKE-EXTRA"]
+ - result.changed == true
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: vpn_ipsec
+
+ - name: TEST - [deleted scoped] Assert IKE-TEST and everything else survived
+ vars:
+ redacted: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ assert:
+ that:
+ - ansible_facts['network_resources']['vpn_ipsec']['ike_group'] == [populate_config.ike_group[0]]
+ - ansible_facts['network_resources']['vpn_ipsec']['esp_group'] == populate_config.esp_group
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['name'] == populate_config.profile[0].name
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['authentication']['mode'] == populate_config.profile[0].authentication.mode
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['authentication']['pre_shared_secret'] == redacted
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['bind_tunnel'] == populate_config.profile[0].bind_tunnel
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['esp_group'] == populate_config.profile[0].esp_group
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['ike_group'] == populate_config.profile[0].ike_group
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tests/cli/empty_config.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/empty_config.yaml
new file mode 100644
index 00000000..7d0329fd
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/empty_config.yaml
@@ -0,0 +1,58 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec 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:
+ 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:
+ 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:
+ 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:
+ 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:
+ 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/tests/cli/gathered.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/gathered.yaml
new file mode 100644
index 00000000..716bc305
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/gathered.yaml
@@ -0,0 +1,28 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec 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:
+ state: gathered
+
+ - name: TEST - [gathered] Assert
+ vars:
+ redacted: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ assert:
+ that:
+ - result.changed == false
+ - result.gathered.esp_group == populate_config.esp_group
+ - result.gathered.profile[0].name == populate_config.profile[0].name
+ - result.gathered.profile[0].authentication.mode == populate_config.profile[0].authentication.mode
+ - result.gathered.profile[0].authentication.pre_shared_secret == redacted
+ - result.gathered.profile[0].bind_tunnel == populate_config.profile[0].bind_tunnel
+ - result.gathered.profile[0].esp_group == populate_config.profile[0].esp_group
+ - result.gathered.profile[0].ike_group == populate_config.profile[0].ike_group
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_vpn_ipsec/tests/cli/merged.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/merged.yaml
new file mode 100644
index 00000000..6c21d107
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/merged.yaml
@@ -0,0 +1,37 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec merged integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: Merge the provided configuration with the existing running configuration
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: &id001
+ config: "{{ merged.config }}"
+ state: merged
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: vpn_ipsec
+
+ - name: Assert that correct set of commands were generated
+ assert:
+ that:
+ - "{{ merged['commands'] | symmetric_difference(result['commands']) | length == 0 }}"
+ - result.changed == true
+ - result.after == ansible_facts['network_resources']['vpn_ipsec']
+ - result.after == merged.after
+ - result.before == {}
+
+ - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: *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/tests/cli/overridden.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/overridden.yaml
new file mode 100644
index 00000000..6a8eed30
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/overridden.yaml
@@ -0,0 +1,36 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec overridden integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Override with only ESP-TEST named -- everything else must be removed
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: &id001
+ config: "{{ overridden.config }}"
+ state: overridden
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: vpn_ipsec
+
+ - name: Assert that correct set of commands were generated
+ assert:
+ that:
+ - "{{ overridden['commands'] | symmetric_difference(result['commands']) | length == 0 }}"
+ - result.changed == true
+ - result.after == ansible_facts['network_resources']['vpn_ipsec']
+ - result.after == overridden.after
+
+ - name: Override with the same configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: *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/tests/cli/parsed.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/parsed.yaml
new file mode 100644
index 00000000..0d1197e4
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/parsed.yaml
@@ -0,0 +1,14 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec parsed integration tests on connection={{ ansible_connection }}
+
+- name: TEST - [parsed] Parse the commands for provided configuration
+ register: result
+ vyos.vyos.vyos_vpn_ipsec:
+ 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/tests/cli/rendered.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/rendered.yaml
new file mode 100644
index 00000000..1babcc2b
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/rendered.yaml
@@ -0,0 +1,15 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec rendered integration tests on connection={{ ansible_connection }}
+
+- block:
+ - name: Structure provided configuration into device specific commands
+ register: result
+ vyos.vyos.vyos_vpn_ipsec:
+ 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/tests/cli/replaced.yaml b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/replaced.yaml
new file mode 100644
index 00000000..0714e8dc
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/tests/cli/replaced.yaml
@@ -0,0 +1,61 @@
+---
+- debug:
+ msg: START vyos_vpn_ipsec replaced integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _populate.yaml
+
+- block:
+ - name: Replace only the named esp_group -- other sections must be untouched
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: &id001
+ config: "{{ replaced.config }}"
+ state: replaced
+
+ - vyos.vyos.vyos_facts:
+ gather_network_resources: vpn_ipsec
+
+ - 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 ESP-TEST was correctly reconciled
+ assert:
+ that:
+ - ansible_facts['network_resources']['vpn_ipsec']['esp_group'] == replaced.after.esp_group
+
+ - name: Assert that un-named sections were left completely untouched
+ vars:
+ redacted: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
+ assert:
+ that:
+ - (ansible_facts['network_resources']['vpn_ipsec']['ike_group'] | sort(attribute='name')) == (populate_config.ike_group | sort(attribute='name'))
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['name'] == populate_config.profile[0].name
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['authentication']['mode'] == populate_config.profile[0].authentication.mode
+ # pre_shared_secret/secret carry no_log: True in the argspec --
+ # gathered facts always redact them to this literal placeholder,
+ # never the real value, so that's what must be asserted here.
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['authentication']['pre_shared_secret'] == redacted
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['bind_tunnel'] == populate_config.profile[0].bind_tunnel
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['esp_group'] == populate_config.profile[0].esp_group
+ - ansible_facts['network_resources']['vpn_ipsec']['profile'][0]['ike_group'] == populate_config.profile[0].ike_group
+ - ansible_facts['network_resources']['vpn_ipsec']['authentication']['psk'][0]['name'] == populate_config.authentication.psk[0].name
+ - ansible_facts['network_resources']['vpn_ipsec']['authentication']['psk'][0]['secret'] == redacted
+ - (ansible_facts['network_resources']['vpn_ipsec']['authentication']['psk'][0]['id'] | sort) == (populate_config.authentication.psk[0].id | sort)
+ - ansible_facts['network_resources']['vpn_ipsec']['authentication']['ppk'][0]['name'] == populate_config.authentication.ppk[0].name
+ - ansible_facts['network_resources']['vpn_ipsec']['authentication']['ppk'][0]['secret'] == redacted
+ - ansible_facts['network_resources']['vpn_ipsec']['authentication']['ppk'][0]['id'] == populate_config.authentication.ppk[0].id
+
+ - name: Replace with the same configuration (IDEMPOTENT)
+ register: result
+ vyos.vyos.vyos_vpn_ipsec: *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/vars/main.yaml b/tests/integration/targets/vyos_vpn_ipsec/vars/main.yaml
new file mode 100644
index 00000000..b86a8d72
--- /dev/null
+++ b/tests/integration/targets/vyos_vpn_ipsec/vars/main.yaml
@@ -0,0 +1,231 @@
+---
+merged:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ compression: true
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-TEST
+ key_exchange: ikev2
+ disable_mobike: true
+ dead_peer_detection:
+ action: restart
+ interval: 15
+ timeout: 60
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ dh_group: 14
+ commands:
+ - set vpn ipsec esp-group ESP-TEST
+ - set vpn ipsec esp-group ESP-TEST compression
+ - set vpn ipsec esp-group ESP-TEST proposal 1
+ - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256
+ - set vpn ipsec esp-group ESP-TEST proposal 1 hash sha256
+ - set vpn ipsec ike-group IKE-TEST
+ - set vpn ipsec ike-group IKE-TEST key-exchange ikev2
+ - set vpn ipsec ike-group IKE-TEST disable-mobike
+ - set vpn ipsec ike-group IKE-TEST dead-peer-detection action restart
+ - set vpn ipsec ike-group IKE-TEST dead-peer-detection interval 15
+ - set vpn ipsec ike-group IKE-TEST dead-peer-detection timeout 60
+ - set vpn ipsec ike-group IKE-TEST proposal 1
+ - set vpn ipsec ike-group IKE-TEST proposal 1 encryption aes256
+ - set vpn ipsec ike-group IKE-TEST proposal 1 hash sha256
+ - set vpn ipsec ike-group IKE-TEST proposal 1 dh-group 14
+ after:
+ esp_group:
+ - name: ESP-TEST
+ compression: true
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-TEST
+ key_exchange: ikev2
+ disable_mobike: true
+ dead_peer_detection:
+ action: restart
+ interval: 15
+ timeout: 60
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ dh_group: 14
+
+# Full pre-populated state used by replaced/overridden/deleted/gathered --
+# matches the same shape as the unit test fixture, so expected
+# commands/after values here are grounded in what's already unit-tested,
+# not invented fresh.
+populate_config:
+ esp_group:
+ - name: ESP-TEST
+ compression: true
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-TEST
+ key_exchange: ikev2
+ disable_mobike: true
+ dead_peer_detection:
+ action: restart
+ interval: 15
+ timeout: 60
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ dh_group: 14
+ - name: IKE-EXTRA
+ key_exchange: ikev1
+ authentication:
+ psk:
+ - name: PSK-TEST
+ id:
+ - local@example.com
+ - remote@example.com
+ secret: test-not-real-secret
+ ppk:
+ - name: PPK-TEST
+ id:
+ - ppk-id-1
+ secret: test-ppk-secret
+ profile:
+ - name: testprofile
+ authentication:
+ mode: pre-shared-secret
+ pre_shared_secret: test-not-real-secret
+ bind_tunnel:
+ - tun0
+ esp_group: ESP-TEST
+ ike_group: IKE-TEST
+ disable_uniqreqids: true
+ interface:
+ - eth0
+ - eth1
+ log:
+ level: 1
+ subsystem:
+ - ike
+ - chd
+ options:
+ flexvpn: true
+ retransmission:
+ attempts: 3
+
+replaced:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+ commands:
+ - delete vpn ipsec esp-group ESP-TEST compression
+ - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128
+ - delete vpn ipsec disable-uniqreqids
+ - delete vpn ipsec interface eth0
+ - delete vpn ipsec interface eth1
+ - delete vpn ipsec log level 1
+ - delete vpn ipsec log subsystem chd
+ - delete vpn ipsec log subsystem ike
+ - delete vpn ipsec options flexvpn
+ - delete vpn ipsec options retransmission attempts 3
+ # NOTE: ike_group/profile/authentication are NOT in `after` deletions --
+ # they're left untouched since replaced only reconciles items it names.
+ # `after` below reflects only what replaced's scope actually changes;
+ # the un-named sections (ike_group, profile, authentication) remain
+ # exactly as populate_config left them and are checked separately in
+ # replaced.yaml via ansible_facts rather than duplicated here.
+ after:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+
+overridden:
+ config:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+ commands:
+ - delete vpn ipsec esp-group ESP-TEST compression
+ - set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128
+ - delete vpn ipsec ike-group IKE-TEST
+ - delete vpn ipsec ike-group IKE-EXTRA
+ - delete vpn ipsec profile testprofile
+ - delete vpn ipsec authentication psk PSK-TEST
+ - delete vpn ipsec authentication ppk PPK-TEST
+ - delete vpn ipsec disable-uniqreqids
+ - delete vpn ipsec interface eth0
+ - delete vpn ipsec interface eth1
+ - delete vpn ipsec log level 1
+ - delete vpn ipsec log subsystem chd
+ - delete vpn ipsec log subsystem ike
+ - delete vpn ipsec options flexvpn
+ - delete vpn ipsec options retransmission attempts 3
+ after:
+ esp_group:
+ - name: ESP-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes128
+ hash: sha256
+
+deleted:
+ commands:
+ - delete vpn ipsec esp-group ESP-TEST
+ - delete vpn ipsec ike-group IKE-TEST
+ - delete vpn ipsec ike-group IKE-EXTRA
+ - delete vpn ipsec profile testprofile
+ - delete vpn ipsec authentication psk PSK-TEST
+ - delete vpn ipsec authentication ppk PPK-TEST
+ - delete vpn ipsec disable-uniqreqids
+ - delete vpn ipsec interface eth0
+ - delete vpn ipsec interface eth1
+ - delete vpn ipsec log level 1
+ - delete vpn ipsec log subsystem chd
+ - delete vpn ipsec log subsystem ike
+ - delete vpn ipsec options flexvpn
+ - delete vpn ipsec options retransmission attempts 3
+ after: {}
+
+rendered:
+ config:
+ esp_group:
+ - name: ESP-RENDER-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ commands:
+ - set vpn ipsec esp-group ESP-RENDER-TEST
+ - set vpn ipsec esp-group ESP-RENDER-TEST proposal 1
+ - set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 encryption aes256
+ - set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 hash sha256
+
+parsed:
+ after:
+ esp_group:
+ - name: ESP-PARSE-TEST
+ proposal:
+ - proposal_id: 1
+ encryption: aes256
+ hash: sha256
+ ike_group:
+ - name: IKE-PARSE-TEST
+ key_exchange: ikev2
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_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg
new file mode 100644
index 00000000..d0d0e19e
--- /dev/null
+++ b/tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg
@@ -0,0 +1,30 @@
+set vpn ipsec authentication ppk PPK-TEST id 'ppk-id-1'
+set vpn ipsec authentication ppk PPK-TEST secret 'test-ppk-secret'
+set vpn ipsec authentication psk PSK-TEST id 'local@example.com'
+set vpn ipsec authentication psk PSK-TEST id 'remote@example.com'
+set vpn ipsec authentication psk PSK-TEST secret 'test-not-real-secret'
+set vpn ipsec disable-uniqreqids
+set vpn ipsec esp-group ESP-TEST compression
+set vpn ipsec esp-group ESP-TEST proposal 1 encryption 'aes256'
+set vpn ipsec esp-group ESP-TEST proposal 1 hash 'sha256'
+set vpn ipsec ike-group IKE-TEST key-exchange 'ikev2'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection action 'restart'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection interval '15'
+set vpn ipsec ike-group IKE-TEST dead-peer-detection timeout '60'
+set vpn ipsec ike-group IKE-TEST disable-mobike
+set vpn ipsec ike-group IKE-TEST proposal 1 dh-group '14'
+set vpn ipsec ike-group IKE-TEST proposal 1 encryption 'aes256'
+set vpn ipsec ike-group IKE-TEST proposal 1 hash 'sha256'
+set vpn ipsec ike-group IKE-EXTRA key-exchange 'ikev1'
+set vpn ipsec interface 'eth0'
+set vpn ipsec interface 'eth1'
+set vpn ipsec log level '1'
+set vpn ipsec log subsystem 'ike'
+set vpn ipsec log subsystem 'chd'
+set vpn ipsec options flexvpn
+set vpn ipsec options retransmission attempts '3'
+set vpn ipsec profile testprofile authentication mode 'pre-shared-secret'
+set vpn ipsec profile testprofile authentication pre-shared-secret 'test-not-real-secret'
+set vpn ipsec profile testprofile bind tunnel 'tun0'
+set vpn ipsec profile testprofile esp-group 'ESP-TEST'
+set vpn ipsec profile testprofile ike-group 'IKE-TEST'
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.py b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py
new file mode 100644
index 00000000..6c616ad1
--- /dev/null
+++ b/tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py
@@ -0,0 +1,568 @@
+#
+# (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 textwrap import dedent
+from unittest.mock import patch
+
+from ansible_collections.vyos.vyos.plugins.modules import vyos_vpn_ipsec
+from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args
+
+from .vyos_module import TestVyosModule, load_fixture
+
+
+class TestVyosVpnIpsecModule(TestVyosModule):
+ module = vyos_vpn_ipsec
+
+ def setUp(self):
+ super(TestVyosVpnIpsecModule, 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.vpn_ipsec.Vpn_ipsecFacts.get_vpn_ipsec_data",
+ )
+ self.execute_show_command = self.mock_execute_show_command.start()
+
+ def tearDown(self):
+ super(TestVyosVpnIpsecModule, 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: two
+ # ike_group entries (IKE-TEST richly configured incl.
+ # dead_peer_detection/disable_mobike, IKE-EXTRA minimal, to prove
+ # "unlisted items preserved"); esp_group with compression+proposal;
+ # psk/ppk with multi-value id lists; a profile; and every top-level
+ # singleton field (disable_uniqreqids, interface, log, options).
+ def load_fixtures(self, commands=None, filename=None):
+ if filename == "EMPTY":
+ self.execute_show_command.side_effect = None
+ self.execute_show_command.return_value = ""
+ return
+
+ if filename is None:
+ filename = "vyos_vpn_ipsec_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_merged_idempotent(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[
+ dict(
+ name="IKE-TEST",
+ key_exchange="ikev2",
+ disable_mobike=True,
+ dead_peer_detection=dict(action="restart", interval=15, timeout=60),
+ proposal=[
+ dict(proposal_id=1, encryption="aes256", hash="sha256", dh_group=14),
+ ],
+ ),
+ ],
+ ),
+ 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_merged_new_group_leaves_existing_untouched(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-NEW",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec esp-group ESP-NEW",
+ "set vpn ipsec esp-group ESP-NEW proposal 1",
+ "set vpn ipsec esp-group ESP-NEW proposal 1 encryption aes256",
+ "set vpn ipsec esp-group ESP-NEW proposal 1 hash sha256",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_bool_field_toggle(self):
+ # disable_mobike explicitly set to True in want when have already
+ # has it True -> no-op; here we flip a different bool
+ # (compression on ESP-TEST is already True in have) by instead
+ # adding a brand new bool-bearing field: options.virtual_ip,
+ # entirely absent from have.
+ playbook = dict(config=dict(options=dict(virtual_ip=True)), state="merged")
+ compare_cmds = ["set vpn ipsec options virtual-ip"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_add_psk_id(self):
+ # multi-value list field: add one id, existing ids must survive
+ playbook = dict(
+ config=dict(
+ authentication=dict(
+ psk=[dict(name="PSK-TEST", id=["third@example.com"])],
+ ),
+ ),
+ state="merged",
+ )
+ compare_cmds = ["set vpn ipsec authentication psk PSK-TEST id third@example.com"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_create_ppk(self):
+ playbook = dict(
+ config=dict(
+ authentication=dict(
+ ppk=[dict(name="PPK-NEW", id=["new-ppk-id"], secret="new-secret")],
+ ),
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec authentication ppk PPK-NEW",
+ "set vpn ipsec authentication ppk PPK-NEW id new-ppk-id",
+ "set vpn ipsec authentication ppk PPK-NEW secret 'new-secret'",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_profile_add_bind_tunnel(self):
+ # multi-value list field on an EXISTING profile: add one tunnel,
+ # existing tun0 must survive
+ playbook = dict(
+ config=dict(
+ profile=[dict(name="testprofile", bind_tunnel=["tun1"])],
+ ),
+ state="merged",
+ )
+ compare_cmds = ["set vpn ipsec profile testprofile bind tunnel tun1"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_merged_create_profile_from_scratch(self):
+ # Regression test: creating a brand new profile must correctly
+ # emit its authentication.mode/pre_shared_secret commands. This
+ # specific path was silently broken (a leftover dotted-Jinja
+ # setval caused render() to return nothing, so addcmd() quietly
+ # dropped both commands with no error) -- caught only by a real
+ # device integration test, since no existing unit test actually
+ # created a profile from scratch or touched its authentication
+ # sub-dict through generate_commands().
+ playbook = dict(
+ config=dict(
+ profile=[
+ dict(
+ name="NEWPROFILE",
+ authentication=dict(
+ mode="pre-shared-secret",
+ pre_shared_secret="brand-new-secret",
+ ),
+ esp_group="ESP-TEST",
+ ike_group="IKE-TEST",
+ ),
+ ],
+ ),
+ state="merged",
+ )
+ compare_cmds = [
+ "set vpn ipsec profile NEWPROFILE",
+ "set vpn ipsec profile NEWPROFILE authentication mode pre-shared-secret",
+ "set vpn ipsec profile NEWPROFILE authentication pre-shared-secret 'brand-new-secret'",
+ "set vpn ipsec profile NEWPROFILE esp-group ESP-TEST",
+ "set vpn ipsec profile NEWPROFILE 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_replaced_only_named_item_touched(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes128", hash="sha256")],
+ ),
+ ],
+ ),
+ state="replaced",
+ )
+ compare_cmds = [
+ # compression omitted from want -> reconciled away (attribute
+ # inside the selected instance is always reset under replaced)
+ "delete vpn ipsec esp-group ESP-TEST compression",
+ "set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128",
+ # ike_group/profile/psk/ppk not named at all -> left alone.
+ # Top-level singleton fields (no instance scope) are always
+ # reconciled under replaced/overridden regardless of what's
+ # named elsewhere.
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_replaced_idempotent(self):
+ # want fully mirrors the ENTIRE fixture state for every top-level
+ # singleton field plus the one named esp_group -- proves
+ # replaced's per-item/per-field reconciliation doesn't spuriously
+ # touch a fully-matching configuration.
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ disable_uniqreqids=True,
+ interface=["eth0", "eth1"],
+ log=dict(level=1, subsystem=["chd", "ike"]),
+ options=dict(flexvpn=True, retransmission=dict(attempts=3)),
+ ),
+ state="replaced",
+ )
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_replaced_scoped_dead_peer_detection(self):
+ # naming ike_group under replaced without dead_peer_detection ->
+ # every DPD field reconciled away (nested-dict attribute reset)
+ playbook = dict(
+ config=dict(
+ ike_group=[dict(name="IKE-TEST", key_exchange="ikev2")],
+ ),
+ state="replaced",
+ )
+ compare_cmds = [
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection action restart",
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection interval 15",
+ "delete vpn ipsec ike-group IKE-TEST dead-peer-detection timeout 60",
+ "delete vpn ipsec ike-group IKE-TEST disable-mobike",
+ "delete vpn ipsec ike-group IKE-TEST proposal 1",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ 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_overridden_removes_unlisted_instances(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes128", hash="sha256")],
+ ),
+ ],
+ ),
+ state="overridden",
+ )
+ compare_cmds = [
+ "delete vpn ipsec esp-group ESP-TEST compression",
+ "set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes128",
+ "delete vpn ipsec ike-group IKE-TEST",
+ "delete vpn ipsec ike-group IKE-EXTRA",
+ "delete vpn ipsec profile testprofile",
+ "delete vpn ipsec authentication psk PSK-TEST",
+ "delete vpn ipsec authentication ppk PPK-TEST",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # deleted -- bare (delete everything)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_deleted_bare(self):
+ playbook = dict(state="deleted")
+ compare_cmds = [
+ "delete vpn ipsec esp-group ESP-TEST",
+ "delete vpn ipsec ike-group IKE-TEST",
+ "delete vpn ipsec ike-group IKE-EXTRA",
+ "delete vpn ipsec profile testprofile",
+ "delete vpn ipsec authentication psk PSK-TEST",
+ "delete vpn ipsec authentication ppk PPK-TEST",
+ "delete vpn ipsec disable-uniqreqids",
+ "delete vpn ipsec interface eth0",
+ "delete vpn ipsec interface eth1",
+ "delete vpn ipsec log level 1",
+ "delete vpn ipsec log subsystem chd",
+ "delete vpn ipsec log subsystem ike",
+ "delete vpn ipsec options flexvpn",
+ "delete vpn ipsec options retransmission attempts 3",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_bare_idempotent(self):
+ playbook = dict(state="deleted")
+ set_module_args(playbook)
+ result = self.execute_module(changed=False, filename="EMPTY")
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ # -------------------------------------------------------------------
+ # deleted -- scoped (delete only what's named)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_deleted_scoped_named_item_only(self):
+ playbook = dict(config=dict(ike_group=[dict(name="IKE-EXTRA")]), state="deleted")
+ compare_cmds = ["delete vpn ipsec ike-group IKE-EXTRA"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_nonexistent_name_idempotent(self):
+ playbook = dict(config=dict(ike_group=[dict(name="IKE-NONEXISTENT")]), state="deleted")
+ set_module_args(playbook)
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), [])
+
+ def test_vyos_vpn_ipsec_deleted_scoped_interface_element(self):
+ # element-level list deletion: name only eth0, eth1 must survive
+ playbook = dict(config=dict(interface=["eth0"]), state="deleted")
+ compare_cmds = ["delete vpn ipsec interface eth0"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_log_subsystem_element(self):
+ playbook = dict(config=dict(log=dict(subsystem=["chd"])), state="deleted")
+ compare_cmds = ["delete vpn ipsec log subsystem chd"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ def test_vyos_vpn_ipsec_deleted_scoped_options_field(self):
+ playbook = dict(config=dict(options=dict(flexvpn=True)), state="deleted")
+ compare_cmds = ["delete vpn ipsec options flexvpn"]
+ set_module_args(playbook)
+ result = self.execute_module(changed=True)
+ self.maxDiff = None
+ self.assertEqual(sorted(result["commands"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # rendered (no device contact -- have is empty regardless of fixture)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_rendered(self):
+ playbook = dict(
+ config=dict(
+ esp_group=[
+ dict(
+ name="ESP-RENDER-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ),
+ state="rendered",
+ )
+ compare_cmds = [
+ "set vpn ipsec esp-group ESP-RENDER-TEST",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 encryption aes256",
+ "set vpn ipsec esp-group ESP-RENDER-TEST proposal 1 hash sha256",
+ ]
+ set_module_args(playbook)
+ result = self.execute_module()
+ self.maxDiff = None
+ self.assertEqual(sorted(result["rendered"]), sorted(compare_cmds))
+
+ # -------------------------------------------------------------------
+ # parsed (reads running_config directly, not the mocked show command)
+ # -------------------------------------------------------------------
+
+ def test_vyos_vpn_ipsec_parsed(self):
+ set_module_args(
+ dict(
+ running_config=dedent(
+ """\
+ set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 encryption aes256
+ set vpn ipsec esp-group ESP-PARSE-TEST proposal 1 hash sha256
+ set vpn ipsec ike-group IKE-PARSE-TEST key-exchange ikev2
+ """,
+ ),
+ state="parsed",
+ ),
+ )
+ parsed = dict(
+ esp_group=[
+ dict(
+ name="ESP-PARSE-TEST",
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[dict(name="IKE-PARSE-TEST", key_exchange="ikev2")],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(result["parsed"], parsed)
+
+ # -------------------------------------------------------------------
+ # gathered
+ # -------------------------------------------------------------------
+
+ def _normalize_scalar_lists(self, node):
+ """Multi-value scalar-list fields (id, subsystem, interface, ...)
+ have no meaningful order -- dict_merge's internal list handling
+ doesn't guarantee a stable sequence between runs. Sort them
+ in-place (recursively) before comparing so tests aren't
+ sensitive to that non-determinism, while list-of-dict
+ collections (already sorted by name in process_facts) are left
+ untouched.
+ """
+ if isinstance(node, dict):
+ for k, v in node.items():
+ if isinstance(v, list) and v and all(not isinstance(i, dict) for i in v):
+ node[k] = sorted(v)
+ else:
+ self._normalize_scalar_lists(v)
+ elif isinstance(node, list):
+ for item in node:
+ self._normalize_scalar_lists(item)
+ return node
+
+ def test_vyos_vpn_ipsec_gathered(self):
+ set_module_args(dict(state="gathered"))
+ gathered = dict(
+ authentication=dict(
+ ppk=[dict(name="PPK-TEST", id=["ppk-id-1"], secret="test-ppk-secret")],
+ psk=[
+ dict(
+ name="PSK-TEST",
+ id=["local@example.com", "remote@example.com"],
+ secret="test-not-real-secret",
+ ),
+ ],
+ ),
+ disable_uniqreqids=True,
+ esp_group=[
+ dict(
+ name="ESP-TEST",
+ compression=True,
+ proposal=[dict(proposal_id=1, encryption="aes256", hash="sha256")],
+ ),
+ ],
+ ike_group=[
+ dict(name="IKE-EXTRA", key_exchange="ikev1"),
+ dict(
+ name="IKE-TEST",
+ key_exchange="ikev2",
+ disable_mobike=True,
+ dead_peer_detection=dict(action="restart", interval=15, timeout=60),
+ proposal=[
+ dict(proposal_id=1, encryption="aes256", hash="sha256", dh_group=14),
+ ],
+ ),
+ ],
+ interface=["eth0", "eth1"],
+ log=dict(level=1, subsystem=["chd", "ike"]),
+ options=dict(flexvpn=True, retransmission=dict(attempts=3)),
+ profile=[
+ dict(
+ name="testprofile",
+ authentication=dict(
+ mode="pre-shared-secret",
+ pre_shared_secret="test-not-real-secret",
+ ),
+ bind_tunnel=["tun0"],
+ esp_group="ESP-TEST",
+ ike_group="IKE-TEST",
+ ),
+ ],
+ )
+ result = self.execute_module(changed=False)
+ self.maxDiff = None
+ self.assertEqual(
+ self._normalize_scalar_lists(result["gathered"]),
+ self._normalize_scalar_lists(gathered),
+ )
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"])

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 26, 10:47 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4284303
Default Alt Text
(391 KB)

Event Timeline