summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-08-21 22:12:11 +1000
committerGitHub <noreply@github.com>2026-08-21 15:12:11 +0300
commit899a6bf7955592ec40670944a860a1bee97b432c (patch)
treefa35031ba10df9ab41376529363664dda0ea7d4c
parentd80f14eef50403f0851c9a12b77890c9bf1f3c4d (diff)
downloadvyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.tar.gz
vyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.zip
T8321: vpn_ipsec modules (#489)
Add VPN IPsec module
-rw-r--r--README.md2
-rw-r--r--changelogs/fragments/T8321_vpn_ipsec.yml4
-rw-r--r--docs/vyos.vyos.vyos_vpn_ipsec_module.rst1553
-rw-r--r--docs/vyos.vyos.vyos_vpn_ipsec_s2s_module.rst1232
-rw-r--r--meta/runtime.yml4
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py206
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py186
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py665
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py355
-rw-r--r--plugins/module_utils/network/vyos/facts/facts.py8
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py113
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py0
-rw-r--r--plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py115
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vpn_ipsec.py997
-rw-r--r--plugins/module_utils/network/vyos/rm_templates/vpn_ipsec_s2s.py670
-rw-r--r--plugins/modules/vyos_vpn_ipsec.py454
-rw-r--r--plugins/modules/vyos_vpn_ipsec_s2s.py337
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/meta/main.yaml1
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tasks/cli.yaml20
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/_parsed.cfg3
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/_populate.yaml8
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/_remove_config.yaml4
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/deleted.yaml69
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/empty_config.yaml58
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/gathered.yaml28
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/merged.yaml37
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/overridden.yaml36
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/parsed.yaml14
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/rendered.yaml15
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/tests/cli/replaced.yaml61
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec/vars/main.yaml231
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/meta/main.yaml1
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/cli.yaml20
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_parsed.cfg1
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_populate.yaml37
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/_remove_config.yaml13
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/deleted.yaml60
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/empty_config.yaml58
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/gathered.yaml20
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/merged.yaml58
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/overridden.yaml39
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/parsed.yaml14
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/rendered.yaml15
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/tests/cli/replaced.yaml42
-rw-r--r--tests/integration/targets/vyos_vpn_ipsec_s2s/vars/main.yaml257
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_config.cfg30
-rw-r--r--tests/unit/modules/network/vyos/fixtures/vyos_vpn_ipsec_s2s_config.cfg26
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vpn_ipsec.py568
-rw-r--r--tests/unit/modules/network/vyos/test_vyos_vpn_ipsec_s2s.py245
58 files changed, 9006 insertions, 0 deletions
diff --git a/README.md b/README.md
index 3e54f27c..bf6a7701 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,8 @@ Name | Description
[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-->
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 9bee831f..c1065dbd 100644
--- a/meta/runtime.yml
+++ b/meta/runtime.yml
@@ -68,5 +68,9 @@ plugin_routing:
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
ha:
redirect: vyos.vyos.vyos_ha
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..75fc1ce5
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec module
+"""
+
+
+class Vpn_ipsecArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "ike_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "close_action": {
+ "type": "str",
+ "choices": ["none", "trap", "start"],
+ },
+ "dead_peer_detection": {
+ "type": "dict",
+ "options": {
+ "action": {
+ "type": "str",
+ "choices": ["trap", "clear", "restart"],
+ },
+ "interval": {"type": "int"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "disable_mobike": {"type": "bool"},
+ "ikev2_reauth": {"type": "bool"},
+ "key_exchange": {
+ "type": "str",
+ "choices": ["ikev1", "ikev2"],
+ },
+ "lifetime": {"type": "int"},
+ "mode": {"type": "str", "choices": ["main", "aggressive"]},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "dh_group": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ "prf": {"type": "str"},
+ },
+ },
+ },
+ },
+ "esp_group": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "compression": {"type": "bool"},
+ "disable_rekey": {"type": "bool"},
+ "life_bytes": {"type": "int"},
+ "life_packets": {"type": "int"},
+ "lifetime": {"type": "int"},
+ "mode": {
+ "type": "str",
+ "choices": ["tunnel", "transport"],
+ },
+ "pfs": {"type": "str"},
+ "proposal": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "proposal_id": {"type": "int"},
+ "encryption": {"type": "str"},
+ "hash": {"type": "str"},
+ },
+ },
+ },
+ },
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "psk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "dhcp_interface": {
+ "type": "list",
+ "elements": "str",
+ },
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ "ppk": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "id": {"type": "list", "elements": "str"},
+ "secret": {"type": "str", "no_log": True},
+ "secret_type": {
+ "type": "str",
+ "choices": ["base64", "hex", "plaintext"],
+ },
+ },
+ },
+ },
+ },
+ "profile": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "mode": {
+ "type": "str",
+ "choices": ["pre-shared-secret"],
+ },
+ "pre_shared_secret": {"type": "str", "no_log": True},
+ },
+ },
+ "bind_tunnel": {"type": "list", "elements": "str"},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "ike_group": {"type": "str"},
+ },
+ },
+ "interface": {"type": "list", "elements": "str"},
+ "log": {
+ "type": "dict",
+ "options": {
+ "level": {"type": "int"},
+ "subsystem": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "options": {
+ "type": "dict",
+ "options": {
+ "disable_route_autoinstall": {"type": "bool"},
+ "flexvpn": {"type": "bool"},
+ "interface": {"type": "str"},
+ "retransmission": {
+ "type": "dict",
+ "options": {
+ "attempts": {"type": "int"},
+ "base": {"type": "float"},
+ "timeout": {"type": "int"},
+ },
+ },
+ "virtual_ip": {"type": "bool"},
+ },
+ },
+ "disable_uniqreqids": {"type": "bool"},
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..bd23d4d3
--- /dev/null
+++ b/plugins/module_utils/network/vyos/argspec/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,186 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+#############################################
+# WARNING #
+#############################################
+#
+# This file is auto generated by the
+# cli_rm_builder.
+#
+# Manually editing this file is not advised.
+#
+# To update the argspec make the desired changes
+# in the module docstring and re-run
+# cli_rm_builder.
+#
+#############################################
+
+"""
+The arg spec for the vyos_vpn_ipsec_s2s module
+"""
+
+
+class Vpn_ipsec_s2sArgs(object): # pylint: disable=R0903
+ """The arg spec for the vyos_vpn_ipsec_s2s module"""
+
+ argument_spec = {
+ "config": {
+ "type": "dict",
+ "options": {
+ "peer": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "name": {"type": "str", "required": True},
+ "disable": {"type": "bool"},
+ "authentication": {
+ "type": "dict",
+ "options": {
+ "local_id": {"type": "str"},
+ "ppk": {
+ "type": "dict",
+ "options": {
+ "id": {"type": "str"},
+ "required": {"type": "bool"},
+ },
+ },
+ "rsa": {
+ "type": "dict",
+ "options": {
+ "local_key": {"type": "str", "no_log": True},
+ "passphrase": {"type": "str", "no_log": True},
+ "remote_key": {"type": "str", "no_log": True},
+ },
+ },
+ "x509": {
+ "type": "dict",
+ "options": {
+ "certificate": {"type": "str"},
+ "passphrase": {"type": "str", "no_log": True},
+ "ca_certificate": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "mode": {
+ "type": "str",
+ "choices": [
+ "pre-shared-secret",
+ "rsa",
+ "x509",
+ ],
+ },
+ "remote_id": {"type": "str"},
+ "use_x509_id": {"type": "bool"},
+ },
+ },
+ "childless": {
+ "type": "str",
+ "choices": ["allow", "prefer", "force", "never"],
+ },
+ "connection_type": {
+ "type": "str",
+ "choices": ["initiate", "trap", "none"],
+ },
+ "default_esp_group": {"type": "str"},
+ "description": {"type": "str"},
+ "dhcp_interface": {"type": "str"},
+ "force_udp_encapsulation": {"type": "bool"},
+ "ike_group": {"type": "str"},
+ "ikev2_reauth": {
+ "type": "str",
+ "choices": ["yes", "no", "inherit"],
+ },
+ "local_address": {"type": "str"},
+ "remote_address": {"type": "list", "elements": "str"},
+ "replay_window": {"type": "int"},
+ "tunnel": {
+ "type": "list",
+ "elements": "dict",
+ "options": {
+ "tunnel_id": {"type": "int", "required": True},
+ "disable": {"type": "bool"},
+ "esp_group": {"type": "str"},
+ "local": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "protocol": {"type": "str"},
+ "priority": {"type": "int"},
+ "remote": {
+ "type": "dict",
+ "options": {
+ "port": {"type": "int"},
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ "virtual_address": {"type": "list", "elements": "str"},
+ "vti": {
+ "type": "dict",
+ "options": {
+ "bind": {"type": "str"},
+ "esp_group": {"type": "str"},
+ "traffic_selector": {
+ "type": "dict",
+ "options": {
+ "local": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ "remote": {
+ "type": "dict",
+ "options": {
+ "prefix": {
+ "type": "list",
+ "elements": "str",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ "running_config": {"type": "str"},
+ "state": {
+ "type": "str",
+ "choices": [
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ "default": "merged",
+ },
+ } # pylint: disable=C0301
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..2d986cb4
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,665 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_vpn_ipsec config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+
+Follows the established per-module convention used by vyos_ha/vyos_nat
+(list-to-dict conversion + explicit per-state branching in
+generate_commands), rather than a shared generic engine.
+
+State semantics (standard Ansible RM convention, confirmed against a
+real device run that caught a bug in an earlier version of this file):
+ - merged: only items/fields named in `want` are touched. Nothing
+ absent from `want` is ever deleted.
+ - replaced: only items NAMED in `want` are touched (same item scope
+ as merged) -- but for each named item, its full state is
+ reconciled to exactly match `want` (fields present in
+ `have` but omitted from `want` ARE deleted). Items not
+ named in `want` at all are left completely alone.
+ - overridden: every item is in scope, including ones absent from
+ `want` entirely -- those get deleted wholesale. Named
+ items are reconciled the same way as `replaced`.
+
+This is implemented via two independent flags:
+ - select_all: whether item iteration considers have-only items too
+ (True only for overridden; False for merged/replaced).
+ - reconcile: whether omitted fields within an already-selected item
+ get deleted (True for replaced/overridden; False for
+ merged/rendered).
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
+ Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsec(ResourceModule):
+ """
+ The vyos_vpn_ipsec config class
+ """
+
+ def __init__(self, module):
+ super(Vpn_ipsec, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vpn_ipsec",
+ tmplt=Vpn_ipsecTemplate(),
+ )
+ self.parsers = [
+ "esp_group",
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ "esp_group.compression",
+ "esp_group.disable_rekey",
+ "esp_group.life_bytes",
+ "esp_group.life_packets",
+ "esp_group.lifetime",
+ "esp_group.mode",
+ "esp_group.pfs",
+ "ike_group",
+ "ike_group.key_exchange",
+ "ike_group.proposal",
+ "ike_group.proposal.dh_group",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.close_action",
+ "ike_group.dead_peer_detection.action",
+ "ike_group.dead_peer_detection.interval",
+ "ike_group.dead_peer_detection.timeout",
+ "ike_group.disable_mobike",
+ "ike_group.ikev2_reauth",
+ "ike_group.lifetime",
+ "ike_group.mode",
+ "profile",
+ "profile.authentication.mode",
+ "profile.authentication.pre_shared_secret",
+ "profile.esp_group",
+ "profile.ike_group",
+ "profile.disable",
+ "authentication.psk.secret_type",
+ "authentication.psk.dhcp_interface",
+ "authentication.ppk",
+ "authentication.ppk.id",
+ "authentication.ppk.secret",
+ "authentication.ppk.secret_type",
+ "interface",
+ "log.level",
+ "log.subsystem",
+ "options.disable_route_autoinstall",
+ "options.flexvpn",
+ "options.interface",
+ "options.retransmission.attempts",
+ "options.retransmission.base",
+ "options.retransmission.timeout",
+ "options.virtual_ip",
+ "disable_uniqreqids",
+ ]
+
+ def execute_module(self):
+ """Execute the module
+
+ :rtype: A dictionary
+ :returns: The result from module execution
+ """
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ """Generate configuration commands to send based on
+ want, have and desired state.
+ """
+ wantd = deepcopy(self.want) or {}
+ haved = deepcopy(self.have) or {}
+
+ for entry in (wantd, haved):
+ self._list_to_dict(entry)
+
+ scoped_delete = None
+ if self.state == "deleted":
+ if wantd:
+ # user named specific items -- surgical removal of just
+ # those, everything else preserved (vyos_vrf precedent:
+ # deleted + instances:[{name: vrf-blue}] removes only
+ # vrf-blue). Capture what was named before wiping wantd.
+ scoped_delete = wantd
+ wantd = {}
+
+ if self.state == "merged":
+ # NOTE: list_to_dict() above must run BEFORE this. dict_merge
+ # concatenates lists rather than merging matching entries by
+ # key, so merging while ike_group/esp_group/etc are still
+ # lists would duplicate entries instead of filling in omitted
+ # fields from `have`. Once they're name-keyed dicts, dict_merge
+ # recurses per-key correctly, which is what lets a partial
+ # update (e.g. specifying only key_exchange) leave other
+ # existing fields on that same group untouched.
+ wantd = dict_merge(haved, wantd)
+
+ select_all = self.state in ("overridden", "deleted")
+ reconcile = self.state in ("replaced", "overridden", "deleted")
+
+ self._compare_esp_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ike_groups(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_profiles(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_psks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_ppks(wantd, haved, select_all, reconcile, scoped_delete)
+ self._compare_top_level(wantd, haved, select_all, reconcile, scoped_delete)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------
+ # List -> name-keyed dict conversion (matches vyos_ha/vyos_nat style)
+ # -------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ for key in ("ike_group", "esp_group", "profile"):
+ items = config.get(key)
+ if isinstance(items, list):
+ config[key] = {item["name"]: item for item in items}
+ for item in config[key].values():
+ if isinstance(item.get("proposal"), list):
+ item["proposal"] = {p["proposal_id"]: p for p in item["proposal"]}
+
+ auth = config.get("authentication", {})
+ for key in ("psk", "ppk"):
+ items = auth.get(key)
+ if isinstance(items, list):
+ auth[key] = {item["name"]: item for item in items}
+
+ # -------------------------------------------------------------------
+ # ESP groups
+ # -------------------------------------------------------------------
+
+ def _compare_esp_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("esp_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("esp_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("esp_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ # only reached when select_all (overridden): item entirely
+ # absent from want -> delete wholesale
+ self.commands.append("delete vpn ipsec esp-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "esp_group", False)
+
+ for field in ("mode", "pfs", "lifetime", "life_bytes", "life_packets"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("compression", "disable_rekey"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "esp_group.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "esp_group.proposal",
+ "esp_group.proposal.encryption",
+ "esp_group.proposal.hash",
+ None,
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # IKE groups
+ # -------------------------------------------------------------------
+
+ def _compare_ike_groups(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_groups = haved.get("ike_group", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("ike_group", {})):
+ if name in have_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ return
+
+ want_groups = wantd.get("ike_group", {})
+ names = set(want_groups) | set(have_groups) if select_all else set(want_groups)
+
+ for name in names:
+ w = want_groups.get(name, {})
+ h = have_groups.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_groups and name not in want_groups:
+ self.commands.append("delete vpn ipsec ike-group {0}".format(name))
+ continue
+
+ if name not in have_groups:
+ self.addcmd({"name": name}, "ike_group", False)
+
+ self._cmp_scalar(
+ w,
+ h,
+ "key_exchange",
+ {"name": name},
+ "ike_group.key_exchange",
+ reconcile,
+ )
+ for field in ("close_action", "lifetime", "mode"):
+ self._cmp_scalar(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+ for field in ("disable_mobike", "ikev2_reauth"):
+ self._cmp_bool(
+ w,
+ h,
+ field,
+ {"name": name},
+ "ike_group.{0}".format(field),
+ reconcile,
+ )
+
+ w_dpd = w.get("dead_peer_detection", {})
+ h_dpd = h.get("dead_peer_detection", {})
+ for field in ("action", "interval", "timeout"):
+ self._cmp_scalar(
+ w_dpd,
+ h_dpd,
+ field,
+ {"name": name},
+ "ike_group.dead_peer_detection.{0}".format(field),
+ reconcile,
+ )
+
+ self._compare_proposals(
+ w.get("proposal", {}),
+ h.get("proposal", {}),
+ {"name": name},
+ "ike_group.proposal",
+ "ike_group.proposal.encryption",
+ "ike_group.proposal.hash",
+ "ike_group.proposal.dh_group",
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Proposals (shared by esp_group / ike_group)
+ # -------------------------------------------------------------------
+
+ def _compare_proposals(
+ self,
+ want_props,
+ have_props,
+ group_ctx,
+ bare_parser,
+ encryption_parser,
+ hash_parser,
+ dh_group_parser,
+ reconcile,
+ ):
+ # a proposal collection lives entirely inside an already-selected
+ # group -- once that group is in scope, its own proposals always
+ # get full reconciliation under replaced/overridden (never a
+ # separate select_all concern of their own).
+ ids = set(want_props) | set(have_props) if reconcile else set(want_props)
+ for pid in ids:
+ w = want_props.get(pid, {})
+ h = have_props.get(pid, {})
+ if w == h:
+ continue
+
+ if pid in have_props and pid not in want_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, True)
+ continue
+
+ if pid not in have_props:
+ self.addcmd(dict(group_ctx, proposal_id=pid), bare_parser, False)
+
+ ctx = dict(group_ctx, proposal_id=pid)
+ self._cmp_scalar(w, h, "encryption", ctx, encryption_parser, reconcile)
+ self._cmp_scalar(w, h, "hash", ctx, hash_parser, reconcile)
+ if dh_group_parser:
+ self._cmp_scalar(w, h, "dh_group", ctx, dh_group_parser, reconcile)
+
+ # -------------------------------------------------------------------
+ # Profiles
+ # -------------------------------------------------------------------
+
+ def _compare_profiles(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_profiles = haved.get("profile", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("profile", {})):
+ if name in have_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ return
+
+ want_profiles = wantd.get("profile", {})
+ names = set(want_profiles) | set(have_profiles) if select_all else set(want_profiles)
+
+ for name in names:
+ w = want_profiles.get(name, {})
+ h = have_profiles.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_profiles and name not in want_profiles:
+ self.commands.append("delete vpn ipsec profile {0}".format(name))
+ continue
+
+ if name not in have_profiles:
+ self.addcmd({"name": name}, "profile", False)
+
+ ctx = {"name": name}
+ w_auth = w.get("authentication", {})
+ h_auth = h.get("authentication", {})
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "mode",
+ ctx,
+ "profile.authentication.mode",
+ reconcile,
+ )
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ "pre_shared_secret",
+ ctx,
+ "profile.authentication.pre_shared_secret",
+ reconcile,
+ )
+ self._cmp_scalar(w, h, "esp_group", ctx, "profile.esp_group", reconcile)
+ self._cmp_scalar(w, h, "ike_group", ctx, "profile.ike_group", reconcile)
+ self._cmp_bool(w, h, "disable", ctx, "profile.disable", reconcile)
+
+ w_tunnels = set(w.get("bind_tunnel") or [])
+ h_tunnels = set(h.get("bind_tunnel") or [])
+ for tun in w_tunnels - h_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", False)
+ if reconcile:
+ for tun in h_tunnels - w_tunnels:
+ self.addcmd(dict(ctx, bind_tunnel=tun), "profile.bind_tunnel", True)
+
+ # -------------------------------------------------------------------
+ # PSKs
+ # -------------------------------------------------------------------
+
+ def _compare_psks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_psks = haved.get("authentication", {}).get("psk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("psk", {})):
+ if name in have_psks:
+ self.commands.append(
+ "delete vpn ipsec authentication psk {0}".format(name),
+ )
+ return
+
+ want_psks = wantd.get("authentication", {}).get("psk", {})
+ names = set(want_psks) | set(have_psks) if select_all else set(want_psks)
+
+ for name in names:
+ w = want_psks.get(name, {})
+ h = have_psks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_psks and name not in want_psks:
+ self.commands.append("delete vpn ipsec authentication psk {0}".format(name))
+ continue
+
+ if name not in have_psks:
+ self.addcmd({"name": name}, "authentication.psk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.psk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.psk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.psk.id", True)
+
+ w_dhcp = set(w.get("dhcp_interface") or [])
+ h_dhcp = set(h.get("dhcp_interface") or [])
+ for i in w_dhcp - h_dhcp:
+ self.addcmd(dict(ctx, dhcp_interface=i), "authentication.psk.dhcp_interface", False)
+ if reconcile:
+ for i in h_dhcp - w_dhcp:
+ self.addcmd(
+ dict(ctx, dhcp_interface=i),
+ "authentication.psk.dhcp_interface",
+ True,
+ )
+
+ def _compare_ppks(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_ppks = haved.get("authentication", {}).get("ppk", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("authentication", {}).get("ppk", {})):
+ if name in have_ppks:
+ self.commands.append(
+ "delete vpn ipsec authentication ppk {0}".format(name),
+ )
+ return
+
+ want_ppks = wantd.get("authentication", {}).get("ppk", {})
+ names = set(want_ppks) | set(have_ppks) if select_all else set(want_ppks)
+
+ for name in names:
+ w = want_ppks.get(name, {})
+ h = have_ppks.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_ppks and name not in want_ppks:
+ self.commands.append("delete vpn ipsec authentication ppk {0}".format(name))
+ continue
+
+ if name not in have_ppks:
+ self.addcmd({"name": name}, "authentication.ppk", False)
+
+ ctx = {"name": name}
+ self._cmp_scalar(w, h, "secret", ctx, "authentication.ppk.secret", reconcile)
+ self._cmp_scalar(
+ w,
+ h,
+ "secret_type",
+ ctx,
+ "authentication.ppk.secret_type",
+ reconcile,
+ )
+
+ w_ids = set(w.get("id") or [])
+ h_ids = set(h.get("id") or [])
+ for i in w_ids - h_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", False)
+ if reconcile:
+ for i in h_ids - w_ids:
+ self.addcmd(dict(ctx, id=i), "authentication.ppk.id", True)
+
+ # -------------------------------------------------------------------
+ # Top-level scalar / list / bool fields
+ #
+ # NOTE: these are all direct fields of the single top-level config
+ # object, not named collections -- there is no "item entirely absent
+ # from want" concept here, only "field omitted from want". So only
+ # `reconcile` applies; `select_all` is irrelevant at this level (it's
+ # accepted for a consistent call signature but unused).
+ # -------------------------------------------------------------------
+
+ def _compare_top_level(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ if scoped_delete is not None:
+ # Principle: naming a parameter under scoped `deleted` means
+ # "delete this specific value" -- a scalar/bool key present
+ # (regardless of value) signals whole-field removal; a list
+ # value present means "delete exactly these elements", not
+ # the whole list, mirroring vyos_vrf's bind_to_all precedent
+ # extended consistently to list- and nested-dict-shaped
+ # fields.
+ if "disable_uniqreqids" in scoped_delete and haved.get("disable_uniqreqids"):
+ self.commands.append("delete vpn ipsec disable-uniqreqids")
+
+ h_ifaces = set(haved.get("interface") or [])
+ for i in set(scoped_delete.get("interface") or []) & h_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ s_log = scoped_delete.get("log", {})
+ h_log = haved.get("log", {})
+ if "level" in s_log and "level" in h_log:
+ self.addcmd({"level": h_log["level"]}, "log.level", True)
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in set(s_log.get("subsystem") or []) & h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ s_opt = scoped_delete.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ if field in s_opt and h_opt.get(field):
+ self.addcmd({}, "options.{0}".format(field), True)
+ if "interface" in s_opt and "interface" in h_opt:
+ self.addcmd({"interface": h_opt["interface"]}, "options.interface", True)
+
+ s_retrans = s_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ if field in s_retrans and field in h_retrans:
+ self.addcmd(
+ {field: h_retrans[field]},
+ "options.retransmission.{0}".format(field),
+ True,
+ )
+ return
+
+ self._cmp_bool(wantd, haved, "disable_uniqreqids", {}, "disable_uniqreqids", reconcile)
+
+ w_ifaces = set(wantd.get("interface") or [])
+ h_ifaces = set(haved.get("interface") or [])
+ for i in w_ifaces - h_ifaces:
+ self.addcmd({"interface": i}, "interface", False)
+ if reconcile:
+ for i in h_ifaces - w_ifaces:
+ self.addcmd({"interface": i}, "interface", True)
+
+ w_log = wantd.get("log", {})
+ h_log = haved.get("log", {})
+ self._cmp_scalar(w_log, h_log, "level", {}, "log.level", reconcile)
+ w_sub = set(w_log.get("subsystem") or [])
+ h_sub = set(h_log.get("subsystem") or [])
+ for s in w_sub - h_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", False)
+ if reconcile:
+ for s in h_sub - w_sub:
+ self.addcmd({"subsystem": s}, "log.subsystem", True)
+
+ w_opt = wantd.get("options", {})
+ h_opt = haved.get("options", {})
+ for field in ("disable_route_autoinstall", "flexvpn", "virtual_ip"):
+ self._cmp_bool(w_opt, h_opt, field, {}, "options.{0}".format(field), reconcile)
+ self._cmp_scalar(w_opt, h_opt, "interface", {}, "options.interface", reconcile)
+
+ w_retrans = w_opt.get("retransmission", {})
+ h_retrans = h_opt.get("retransmission", {})
+ for field in ("attempts", "base", "timeout"):
+ self._cmp_scalar(
+ w_retrans,
+ h_retrans,
+ field,
+ {},
+ "options.retransmission.{0}".format(field),
+ reconcile,
+ )
+
+ # -------------------------------------------------------------------
+ # Field-level helpers (mirrors vyos_nat's _cmp_scalar / _cmp_bool)
+ # -------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif reconcile and h is not None:
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False):
+ # An explicitly-specified value (even False) is always enforced,
+ # regardless of state -- that's the user directly saying what
+ # they want. An OMITTED field is only enforced (i.e. deleted if
+ # currently True) under full reconciliation (replaced/overridden).
+ # Under merged, an omitted field is left alone -- protected
+ # further upstream by dict_merge backfilling `want` from `have`
+ # before this is ever reached, but this still needs to be correct
+ # in isolation (e.g. for a field nested inside a dict that wasn't
+ # part of the dict_merge'd top-level structure).
+ explicit = field in want
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h and (w or explicit or reconcile):
+ self.addcmd(dict(ctx), parser, not w)
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..65232ed7
--- /dev/null
+++ b/plugins/module_utils/network/vyos/config/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,355 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos_vpn_ipsec_s2s config file.
+It is in this file where the current configuration (as dict)
+is compared to the provided configuration (as dict) and the command set
+necessary to bring the current configuration to its desired end-state is
+created.
+
+Mirrors vyos_vpn_ipsec's config.py exactly -- same list-to-dict
+conversion + explicit per-state branching, same select_all/reconcile
+two-flag design for the replaced/overridden distinction, same scoped
+deleted handling. See that file's own docstring for the full state
+semantics; the summary:
+
+ - merged: only items/fields named in `want` are touched.
+ - replaced: only items NAMED in `want` are touched, but each named
+ item is fully reconciled (omitted fields removed).
+ - overridden: every item is in scope, including ones absent from
+ `want` -- those get deleted wholesale. Named items
+ reconciled the same way as replaced.
+ - deleted: bare (no config) deletes everything; a scoped config
+ deletes only what's named, down to individual list
+ elements.
+"""
+
+from copy import deepcopy
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.rm_base.resource_module import (
+ ResourceModule,
+)
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
+ dict_merge,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
+ Facts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sTemplate,
+)
+
+
+class Vpn_ipsec_s2s(ResourceModule):
+ """
+ The vyos_vpn_ipsec_s2s config class
+ """
+
+ def __init__(self, module):
+ super(Vpn_ipsec_s2s, self).__init__(
+ empty_fact_val={},
+ facts_module=Facts(module),
+ module=module,
+ resource="vpn_ipsec_s2s",
+ tmplt=Vpn_ipsec_s2sTemplate(),
+ )
+
+ def execute_module(self):
+ if self.state not in ["parsed", "gathered"]:
+ self.generate_commands()
+ self.run_commands()
+ return self.result
+
+ def generate_commands(self):
+ wantd = deepcopy(self.want) or {}
+ haved = deepcopy(self.have) or {}
+
+ for entry in (wantd, haved):
+ self._list_to_dict(entry)
+
+ scoped_delete = None
+ if self.state == "deleted":
+ if wantd:
+ scoped_delete = wantd
+ wantd = {}
+
+ if self.state == "merged":
+ # list_to_dict() above must run BEFORE this -- dict_merge
+ # concatenates raw lists rather than merging matching items
+ # by key, so it only does the right thing once both sides
+ # are already name-keyed dicts.
+ wantd = dict_merge(haved, wantd)
+
+ select_all = self.state in ("overridden", "deleted")
+ reconcile = self.state in ("replaced", "overridden", "deleted")
+
+ self._compare_peers(wantd, haved, select_all, reconcile, scoped_delete)
+
+ self.commands = list(dict.fromkeys(self.commands))
+
+ # -------------------------------------------------------------------
+ # List -> name-keyed dict conversion
+ # -------------------------------------------------------------------
+
+ def _list_to_dict(self, config):
+ peers = config.get("peer")
+ if isinstance(peers, list):
+ config["peer"] = {p["name"]: p for p in peers}
+ for peer in config["peer"].values():
+ if isinstance(peer.get("tunnel"), list):
+ peer["tunnel"] = {t["tunnel_id"]: t for t in peer["tunnel"]}
+
+ # -------------------------------------------------------------------
+ # Peers
+ # -------------------------------------------------------------------
+
+ def _compare_peers(self, wantd, haved, select_all, reconcile, scoped_delete=None):
+ have_peers = haved.get("peer", {})
+
+ if scoped_delete is not None:
+ for name in set(scoped_delete.get("peer", {})):
+ if name in have_peers:
+ self.commands.append(
+ "delete vpn ipsec site-to-site peer {0}".format(name),
+ )
+ return
+
+ want_peers = wantd.get("peer", {})
+ names = set(want_peers) | set(have_peers) if select_all else set(want_peers)
+
+ for name in names:
+ w = want_peers.get(name, {})
+ h = have_peers.get(name, {})
+ if w == h:
+ continue
+
+ if name in have_peers and name not in want_peers:
+ self.commands.append(
+ "delete vpn ipsec site-to-site peer {0}".format(name),
+ )
+ continue
+
+ if name not in have_peers:
+ self.addcmd({"name": name}, "peer", False)
+
+ ctx = {"name": name}
+ self._cmp_bool(w, h, "disable", ctx, "peer.disable", reconcile)
+
+ w_auth = w.get("authentication", {})
+ h_auth = h.get("authentication", {})
+ for field in ("local_id", "remote_id", "mode"):
+ self._cmp_scalar(
+ w_auth,
+ h_auth,
+ field,
+ ctx,
+ "peer.authentication.{0}".format(field),
+ reconcile,
+ )
+ self._cmp_bool(
+ w_auth,
+ h_auth,
+ "use_x509_id",
+ ctx,
+ "peer.authentication.use_x509_id",
+ reconcile,
+ )
+
+ w_ppk = w_auth.get("ppk", {})
+ h_ppk = h_auth.get("ppk", {})
+ self._cmp_scalar(w_ppk, h_ppk, "id", ctx, "peer.authentication.ppk.id", reconcile)
+ self._cmp_bool(
+ w_ppk,
+ h_ppk,
+ "required",
+ ctx,
+ "peer.authentication.ppk.required",
+ reconcile,
+ )
+
+ w_rsa = w_auth.get("rsa", {})
+ h_rsa = h_auth.get("rsa", {})
+ for field in ("local_key", "remote_key", "passphrase"):
+ self._cmp_scalar(
+ w_rsa,
+ h_rsa,
+ field,
+ ctx,
+ "peer.authentication.rsa.{0}".format(field),
+ reconcile,
+ )
+
+ w_x509 = w_auth.get("x509", {})
+ h_x509 = h_auth.get("x509", {})
+ for field in ("certificate", "passphrase"):
+ self._cmp_scalar(
+ w_x509,
+ h_x509,
+ field,
+ ctx,
+ "peer.authentication.x509.{0}".format(field),
+ reconcile,
+ )
+ w_ca = set(w_x509.get("ca_certificate") or [])
+ h_ca = set(h_x509.get("ca_certificate") or [])
+ for cert in w_ca - h_ca:
+ self.addcmd(
+ dict(ctx, ca_certificate=cert),
+ "peer.authentication.x509.ca_certificate",
+ False,
+ )
+ if reconcile:
+ for cert in h_ca - w_ca:
+ self.addcmd(
+ dict(ctx, ca_certificate=cert),
+ "peer.authentication.x509.ca_certificate",
+ True,
+ )
+
+ for field in (
+ "childless",
+ "connection_type",
+ "default_esp_group",
+ "description",
+ "dhcp_interface",
+ "ike_group",
+ "ikev2_reauth",
+ "local_address",
+ ):
+ self._cmp_scalar(w, h, field, ctx, "peer.{0}".format(field), reconcile)
+ self._cmp_bool(
+ w,
+ h,
+ "force_udp_encapsulation",
+ ctx,
+ "peer.force_udp_encapsulation",
+ reconcile,
+ )
+ self._cmp_scalar(w, h, "replay_window", ctx, "peer.replay_window", reconcile)
+
+ w_remote_addr = set(w.get("remote_address") or [])
+ h_remote_addr = set(h.get("remote_address") or [])
+ for addr in w_remote_addr - h_remote_addr:
+ self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", False)
+ if reconcile:
+ for addr in h_remote_addr - w_remote_addr:
+ self.addcmd(dict(ctx, remote_address=addr), "peer.remote_address", True)
+
+ w_virt_addr = set(w.get("virtual_address") or [])
+ h_virt_addr = set(h.get("virtual_address") or [])
+ for addr in w_virt_addr - h_virt_addr:
+ self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", False)
+ if reconcile:
+ for addr in h_virt_addr - w_virt_addr:
+ self.addcmd(dict(ctx, virtual_address=addr), "peer.virtual_address", True)
+
+ self._compare_tunnels(w.get("tunnel", {}), h.get("tunnel", {}), ctx, reconcile)
+ self._compare_vti(w.get("vti", {}), h.get("vti", {}), ctx, reconcile)
+
+ # -------------------------------------------------------------------
+ # Tunnels (nested collection within a peer)
+ # -------------------------------------------------------------------
+
+ def _compare_tunnels(self, want_tunnels, have_tunnels, peer_ctx, reconcile):
+ # A tunnel collection lives entirely inside an already-selected
+ # peer -- once that peer is in scope, its own tunnels always get
+ # full reconciliation under replaced/overridden, matching how
+ # esp_group/ike_group's own nested proposals behave in the
+ # profile module.
+ ids = set(want_tunnels) | set(have_tunnels) if reconcile else set(want_tunnels)
+ for tid in ids:
+ w = want_tunnels.get(tid, {})
+ h = have_tunnels.get(tid, {})
+ if w == h:
+ continue
+
+ if tid in have_tunnels and tid not in want_tunnels:
+ self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", True)
+ continue
+
+ if tid not in have_tunnels:
+ self.addcmd(dict(peer_ctx, tunnel_id=tid), "peer.tunnel", False)
+
+ ctx = dict(peer_ctx, tunnel_id=tid)
+ self._cmp_bool(w, h, "disable", ctx, "peer.tunnel.disable", reconcile)
+ for field in ("esp_group", "protocol"):
+ self._cmp_scalar(w, h, field, ctx, "peer.tunnel.{0}".format(field), reconcile)
+ self._cmp_scalar(w, h, "priority", ctx, "peer.tunnel.priority", reconcile)
+
+ for side in ("local", "remote"):
+ w_side = w.get(side, {})
+ h_side = h.get(side, {})
+ self._cmp_scalar(
+ w_side,
+ h_side,
+ "port",
+ ctx,
+ "peer.tunnel.{0}.port".format(side),
+ reconcile,
+ )
+ w_prefix = set(w_side.get("prefix") or [])
+ h_prefix = set(h_side.get("prefix") or [])
+ for p in w_prefix - h_prefix:
+ self.addcmd(
+ dict(ctx, prefix=p),
+ "peer.tunnel.{0}.prefix".format(side),
+ False,
+ )
+ if reconcile:
+ for p in h_prefix - w_prefix:
+ self.addcmd(
+ dict(ctx, prefix=p),
+ "peer.tunnel.{0}.prefix".format(side),
+ True,
+ )
+
+ # -------------------------------------------------------------------
+ # VTI (nested dict within a peer, not a collection)
+ # -------------------------------------------------------------------
+
+ def _compare_vti(self, w_vti, h_vti, peer_ctx, reconcile):
+ for field in ("bind", "esp_group"):
+ self._cmp_scalar(w_vti, h_vti, field, peer_ctx, "peer.vti.{0}".format(field), reconcile)
+
+ w_ts = w_vti.get("traffic_selector", {})
+ h_ts = h_vti.get("traffic_selector", {})
+ for side in ("local", "remote"):
+ w_prefix = set(w_ts.get(side, {}).get("prefix") or [])
+ h_prefix = set(h_ts.get(side, {}).get("prefix") or [])
+ parser = "peer.vti.traffic_selector.{0}.prefix".format(side)
+ for p in w_prefix - h_prefix:
+ self.addcmd(dict(peer_ctx, prefix=p), parser, False)
+ if reconcile:
+ for p in h_prefix - w_prefix:
+ self.addcmd(dict(peer_ctx, prefix=p), parser, True)
+
+ # -------------------------------------------------------------------
+ # Field-level helpers (mirrors vyos_vpn_ipsec's own)
+ # -------------------------------------------------------------------
+
+ def _cmp_scalar(self, want, have, field, ctx, parser, reconcile=False):
+ w = want.get(field)
+ h = have.get(field)
+ if w != h:
+ if w is not None:
+ self.addcmd(dict(ctx, **{field: w}), parser, False)
+ elif reconcile and h is not None:
+ self.addcmd(dict(ctx, **{field: h}), parser, True)
+
+ def _cmp_bool(self, want, have, field, ctx, parser, reconcile=False):
+ explicit = field in want
+ w = bool(want.get(field))
+ h = bool(have.get(field))
+ if w != h and (w or explicit or reconcile):
+ self.addcmd(dict(ctx), parser, not w)
diff --git a/plugins/module_utils/network/vyos/facts/facts.py b/plugins/module_utils/network/vyos/facts/facts.py
index 2a789103..69cb0d0b 100644
--- a/plugins/module_utils/network/vyos/facts/facts.py
+++ b/plugins/module_utils/network/vyos/facts/facts.py
@@ -83,6 +83,12 @@ from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.snmp_
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.static_routes.static_routes import (
Static_routesFacts,
)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecFacts,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vpn_ipsec_s2s.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sFacts,
+)
from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.vrf.vrf import VrfFacts
FACT_LEGACY_SUBSETS = dict(default=Default, neighbors=Neighbors, config=Config)
@@ -109,6 +115,8 @@ FACT_RESOURCE_SUBSETS = dict(
snmp_server=Snmp_serverFacts,
hostname=HostnameFacts,
vrf=VrfFacts,
+ vpn_ipsec=Vpn_ipsecFacts,
+ vpn_ipsec_s2s=Vpn_ipsec_s2sFacts,
)
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
new file mode 100644
index 00000000..695b9b8d
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec/vpn_ipsec.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha (explicit process_facts() naming each
+name-keyed dict that needs converting to a list), matching the config.py
+convention for this module, rather than a generic argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec.vpn_ipsec import (
+ Vpn_ipsecArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec import (
+ Vpn_ipsecTemplate,
+)
+
+
+class Vpn_ipsecFacts(object):
+ """The vyos vpn_ipsec facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsecArgs.argument_spec
+
+ def get_vpn_ipsec_data(self, connection):
+ return connection.get('show configuration commands | match "vpn ipsec"')
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects. Each key handled explicitly, matching
+ the vyos_logging_global/vyos_ha convention.
+ """
+ if not objFinal:
+ return objFinal
+
+ for key in ("ike_group", "esp_group"):
+ if key in objFinal:
+ items = list(objFinal[key].values())
+ for item in items:
+ if "proposal" in item:
+ item["proposal"] = sorted(
+ item["proposal"].values(),
+ key=lambda p: int(p["proposal_id"]),
+ )
+ objFinal[key] = sorted(items, key=lambda item: item["name"])
+
+ if "profile" in objFinal:
+ objFinal["profile"] = sorted(
+ objFinal["profile"].values(),
+ key=lambda item: item["name"],
+ )
+
+ if "authentication" in objFinal:
+ auth = objFinal["authentication"]
+ for key in ("psk", "ppk"):
+ if key in auth:
+ auth[key] = sorted(
+ auth[key].values(),
+ key=lambda item: item["name"],
+ )
+
+ return objFinal
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_data(connection)
+
+ vpn_ipsec_parser = Vpn_ipsecTemplate(lines=data.splitlines(), module=self._module)
+ objs = vpn_ipsec_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/__init__.py
diff --git a/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
new file mode 100644
index 00000000..6c9b5d8e
--- /dev/null
+++ b/plugins/module_utils/network/vyos/facts/vpn_ipsec_s2s/vpn_ipsec_s2s.py
@@ -0,0 +1,115 @@
+#
+# -*- coding: utf-8 -*-
+# Copyright 2026 Red Hat
+# GNU General Public License v3.0+
+# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+"""
+The vyos vpn_ipsec_s2s fact class
+It is in this file the configuration is collected from the device
+for a given resource, parsed, and the facts tree is populated
+based on the configuration.
+
+Follows the established per-key conversion convention used by
+vyos_logging_global/vyos_ha/vyos_vpn_ipsec (explicit process_facts()
+naming each name-keyed dict that needs converting to a list), matching
+the config.py convention for this module, rather than a generic
+argspec-driven walker.
+"""
+
+from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import (
+ utils,
+)
+
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.vpn_ipsec_s2s.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sArgs,
+)
+from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.rm_templates.vpn_ipsec_s2s import (
+ Vpn_ipsec_s2sTemplate,
+)
+
+
+class Vpn_ipsec_s2sFacts(object):
+ """The vyos vpn_ipsec_s2s facts class"""
+
+ def __init__(self, module, subspec="config", options="options"):
+ self._module = module
+ self.argument_spec = Vpn_ipsec_s2sArgs.argument_spec
+
+ def get_vpn_ipsec_s2s_data(self, connection):
+ return connection.get(
+ 'show configuration commands | match "vpn ipsec site-to-site"',
+ )
+
+ def process_facts(self, objFinal):
+ """Convert the name-keyed dicts produced by the parser into the
+ lists the argspec expects.
+
+ NOTE: every PARSERS result template in rm_templates.py nests its
+ output under "site_to_site" -> "peer" (mirroring the CLI's own
+ tree: `vpn ipsec site-to-site peer <name> ...`), but the
+ argspec's `config` has `peer` directly at the top level -- there
+ is no `site_to_site` wrapper in the argspec, since that's the
+ one node wrap_docstring.py unwrapped when building the
+ docstring (its own children became config's children directly).
+ So this needs to strip that outer key, not just convert the
+ name-keyed dicts to lists.
+ """
+ if not objFinal:
+ return objFinal
+
+ site_to_site = objFinal.get("site_to_site", {})
+ peers = site_to_site.get("peer", {})
+
+ items = list(peers.values())
+ for item in items:
+ if "tunnel" in item:
+ item["tunnel"] = sorted(
+ item["tunnel"].values(),
+ key=lambda t: int(t["tunnel_id"]),
+ )
+
+ return {"peer": sorted(items, key=lambda item: item["name"])}
+
+ def populate_facts(self, connection, ansible_facts, data=None):
+ """Populate the facts for Vpn_ipsec_s2s network resource
+
+ :param connection: the device connection
+ :param ansible_facts: Facts dictionary
+ :param data: previously collected conf
+
+ :rtype: dictionary
+ :returns: facts
+ """
+ facts = {}
+
+ if not data:
+ data = self.get_vpn_ipsec_s2s_data(connection)
+
+ vpn_ipsec_s2s_parser = Vpn_ipsec_s2sTemplate(
+ lines=data.splitlines(),
+ module=self._module,
+ )
+ objs = vpn_ipsec_s2s_parser.parse()
+
+ ansible_facts["ansible_network_resources"].pop("vpn_ipsec_s2s", None)
+ objs = self.process_facts(objs)
+
+ params = utils.remove_empties(
+ vpn_ipsec_s2s_parser.validate_config(
+ self.argument_spec,
+ {"config": objs},
+ redact=True,
+ ),
+ )
+
+ facts["vpn_ipsec_s2s"] = params.get("config", {})
+ ansible_facts["ansible_network_resources"].update(facts)
+
+ return ansible_facts
diff --git a/plugins/module_utils/network/vyos/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"])