diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-21 22:12:11 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-21 15:12:11 +0300 |
| commit | 899a6bf7955592ec40670944a860a1bee97b432c (patch) | |
| tree | fa35031ba10df9ab41376529363664dda0ea7d4c | |
| parent | d80f14eef50403f0851c9a12b77890c9bf1f3c4d (diff) | |
| download | vyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.tar.gz vyos.vyos-899a6bf7955592ec40670944a860a1bee97b432c.zip | |
T8321: vpn_ipsec modules (#489)
Add VPN IPsec module
58 files changed, 9006 insertions, 0 deletions
@@ -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 "vpn ipsec"</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'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> ←</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;">['set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256', 'set vpn ipsec ike-group IKE-TEST key-exchange ikev2']</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;">['set vpn ipsec esp-group ESP-TEST proposal 1 encryption aes256']</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'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 "any".</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 "any".</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'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'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 "vpn ipsec site-to-site"</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'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> ←</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;">["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'"]</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;">["set vpn ipsec site-to-site peer PEER-TEST ike-group 'IKE-TEST'"]</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"]) |
