diff options
| author | omnom62 <75066712+omnom62@users.noreply.github.com> | 2026-08-22 01:04:18 +1000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-21 10:04:18 -0500 |
| commit | cb738721c15ac01f49f66144dae80eb478331f77 (patch) | |
| tree | 52f870ed414506aaa5cd6efa7ae6513d807e194b | |
| parent | 885b9462480712210ddeea9ca4a4b7a52e9ef587 (diff) | |
| download | rest.vyos-cb738721c15ac01f49f66144dae80eb478331f77.tar.gz rest.vyos-cb738721c15ac01f49f66144dae80eb478331f77.zip | |
T8989: wave 2 modules: interfaces, l3_interfaces, lag_interfaces, lldp_interfaces, ospfv2/3, ospf_interfaces
* T8989: vyos_interfaces
* T8989: vyos_interfaces changelog
* T8989: vyos_interfaces changelog
* T8989: changelog typo
* T8989: vyos_l3_interfaces module
* T8989: vyos_l3_interfaces module SIT and UAT
* T8989: vyos_l3_interfaces doc
* T8989: lag_interfaces
* T8989: lag interfaces
* T8989: lldp_interfaces
* T8989: lldp_interfaces module
* T8989: SIT updated
* T8989: SIT updated
* T8989: SIT updated
* T8989: typo fixes
* T8989: SIT and UAT updates
* T8989: ospf_v3 module
* T8989: ospf_v3 module
* T8989: vyos_ospf_interfaces
* T8989: vyos_ospf_interfaces SIT
* T8989: vyos_ospf_interfaces SIT
* T8989: vyos_ospfv3 SIT and UAT
---------
Co-authored-by: John Estabrook <jestabro@vyos.io>
142 files changed, 11843 insertions, 65 deletions
@@ -79,18 +79,20 @@ Name | Description [vyos.rest.vyos_banner](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_banner_module.rst)|Manage multiline banners on VyOS devices via REST API. [vyos.rest.vyos_configure](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_configure_module.rst)|Send raw set/delete commands to a VyOS device via REST API. [vyos.rest.vyos_hostname](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_hostname_module.rst)|Manage the system hostname on a VyOS device via the REST API. +[vyos.rest.vyos_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_interfaces_module.rst)|Manage interface configuration on VyOS devices via REST API. +[vyos.rest.vyos_l3_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_l3_interfaces_module.rst)|Manage L3 interface configuration on VyOS devices via REST API. +[vyos.rest.vyos_lag_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lag_interfaces_module.rst)|Manage LAG interface configuration on VyOS devices via REST API. [vyos.rest.vyos_lldp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_global_module.rst)|Manage LLDP global configuration on VyOS via REST API. -<<<<<<< HEAD +[vyos.rest.vyos_lldp_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_int +erfaces_module.rst)|Manage LLDP interface configuration on VyOS devices via REST API. [vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API [vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API +[vyos.rest.vyos_ospf_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospf_interfaces_module.rst)|Manage OSPF interface configuration on VyOS devices using REST API +[vyos.rest.vyos_ospfv2](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospfv2_module.rst)|Manage OSPFv2 configuration on VyOS devices using REST API +[vyos.rest.vyos_ospfv3](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospfv3_module.rst)|Manage OSPFv3 configuration on VyOS devices using REST API [vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|Manage prefix-list configuration on VyOS devices using REST API [vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API [vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API -[vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API. -[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API. -[vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|⚠️ Manage prefix-list configuration on VyOS devices using REST API. *(not yet available)* -[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API. -[vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API. [vyos.rest.vyos_static_routes](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_static_routes_module.rst)|Manage static routes on VyOS devices via REST API. <!--end collection content--> diff --git a/changelogs/fragments/t8989-vyos_interfaces.yml b/changelogs/fragments/t8989-vyos_interfaces.yml new file mode 100644 index 0000000..856575d --- /dev/null +++ b/changelogs/fragments/t8989-vyos_interfaces.yml @@ -0,0 +1,4 @@ +--- +minor_changes: + - vyos_interfaces - Add support for L2 interface configuration + - vyos_l3_interfaces - Add support for L3 interfaces configuration diff --git a/docs/vyos.rest.vyos_interfaces_module.rst b/docs/vyos.rest.vyos_interfaces_module.rst new file mode 100644 index 0000000..8438d9d --- /dev/null +++ b/docs/vyos.rest.vyos_interfaces_module.rst @@ -0,0 +1,356 @@ +.. _vyos.rest.vyos_interfaces_module: + + +************************* +vyos.rest.vyos_interfaces +************************* + +**Manage interface configuration on VyOS devices via REST API.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages L2 interface configuration (description, MTU, speed, duplex, enabled) on VyOS devices using the HTTPS REST API. +- IP address configuration is handled by :ref:`vyos.rest.vyos_l3_interfaces <vyos.rest.vyos_l3_interfaces_module>`. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="2">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="2"> + <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">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of interface configurations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <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>Interface description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>duplex</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>auto</li> + <li>full</li> + <li>half</li> + </ul> + </td> + <td> + <div>Interface duplex setting.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>enabled</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><div style="color: blue"><b>yes</b> ←</div></li> + </ul> + </td> + <td> + <div>Whether the interface is enabled. False sets the disable flag.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mtu</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>Interface MTU.</div> + </td> + </tr> + <tr> + <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>Full interface name (e.g. eth0, bond0, lo).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>speed</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>auto</li> + <li>10</li> + <li>100</li> + <li>1000</li> + <li>2500</li> + <li>10000</li> + </ul> + </td> + <td> + <div>Interface speed setting.</div> + </td> + </tr> + + <tr> + <td colspan="2"> + <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> + </ul> + </td> + <td> + <div><code>merged</code> - Merge config with existing interface settings.</div> + <div><code>replaced</code> - Replace config for listed interfaces.</div> + <div><code>overridden</code> - Replace config for all interfaces.</div> + <div><code>deleted</code> - Remove listed interface config or all interface config.</div> + <div><code>gathered</code> - Read interface config from device without changes.</div> + </td> + </tr> + </table> + <br/> + + + +See Also +-------- + +.. seealso:: + + :ref:`vyos.vyos.vyos_interfaces_module` + The official documentation on the **vyos.vyos.vyos_interfaces** module. + :ref:`vyos.rest.vyos_l3_interfaces_module` + The official documentation on the **vyos.rest.vyos_l3_interfaces** module. + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge interface configuration + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Management interface + mtu: 1500 + enabled: true + state: merged + + - name: Disable an interface + vyos.rest.vyos_interfaces: + config: + - name: eth1 + enabled: false + state: merged + + - name: Delete interface description + vyos.rest.vyos_interfaces: + config: + - name: eth0 + state: deleted + + - name: Gather current interface configuration + vyos.rest.vyos_interfaces: + state: gathered + + + +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">list</span> + </div> + </td> + <td>when changed</td> + <td> + <div>Interface configuration after this module ran.</div> + <br/> + </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">list</span> + </div> + </td> + <td>always</td> + <td> + <div>Interface configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is gathered</td> + <td> + <div>Current interface configuration as structured data.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_l3_interfaces_module.rst b/docs/vyos.rest.vyos_l3_interfaces_module.rst new file mode 100644 index 0000000..d251abb --- /dev/null +++ b/docs/vyos.rest.vyos_l3_interfaces_module.rst @@ -0,0 +1,494 @@ +.. _vyos.rest.vyos_l3_interfaces_module: + + +**************************** +vyos.rest.vyos_l3_interfaces +**************************** + +**Manage L3 interface configuration on VyOS devices via REST API.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages IPv4 and IPv6 address configuration on VyOS interfaces using the HTTPS REST API. +- Mirrors ``vyos.vyos.vyos_l3_interfaces`` but uses the HTTP API instead of CLI. + + + + +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">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of L3 interface configurations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv4</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 IPv4 addresses of the interface.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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 address in CIDR notation or <code>dhcp</code>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv6</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 IPv6 addresses of the interface.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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>IPv6 address in CIDR notation, <code>dhcpv6</code>, or <code>auto-config</code>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <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>Full name of the interface, e.g. eth0, lo.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>vifs</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 virtual sub-interfaces (VLANs).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv4</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 IPv4 addresses of the VIF.</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>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 address in CIDR notation or <code>dhcp</code>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ipv6</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 IPv6 addresses of the VIF.</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>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>IPv6 address in CIDR notation, <code>dhcpv6</code>, or <code>auto-config</code>.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>vlan_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>VLAN identifier.</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>Used only with state <code>parsed</code>.</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><code>merged</code> - Add addresses without removing existing ones.</div> + <div><code>replaced</code> - Replace addresses for listed interfaces.</div> + <div><code>overridden</code> - Replace addresses for all interfaces.</div> + <div><code>deleted</code> - Remove listed or all interface addresses.</div> + <div><code>gathered</code> - Read interface addresses from device without changes.</div> + <div><code>rendered</code> - Return commands for provided config without connecting.</div> + <div><code>parsed</code> - Parse running_config into structured data.</div> + </td> + </tr> + </table> + <br/> + + + +See Also +-------- + +.. seealso:: + + :ref:`vyos.vyos.vyos_l3_interfaces_module` + The official documentation on the **vyos.vyos.vyos_l3_interfaces** module. + :ref:`vyos.rest.vyos_interfaces_module` + The official documentation on the **vyos.rest.vyos_interfaces** module. + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge L3 interface configuration + vyos.rest.vyos_l3_interfaces: + config: + - name: eth0 + ipv4: + - address: 192.0.2.1/24 + - address: dhcp + - name: lo + ipv4: + - address: 10.0.0.1/32 + ipv6: + - address: 2001:db8::1/128 + state: merged + + - name: Add VLAN subinterface addresses + vyos.rest.vyos_l3_interfaces: + config: + - name: eth0 + vifs: + - vlan_id: 100 + ipv4: + - address: 192.0.2.100/24 + state: merged + + - name: Delete all interface addresses + vyos.rest.vyos_l3_interfaces: + state: deleted + + - name: Gather current L3 interface configuration + vyos.rest.vyos_l3_interfaces: + state: gathered + + + +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">list</span> + </div> + </td> + <td>when changed</td> + <td> + <div>L3 interface configuration after this module ran.</div> + <br/> + </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">list</span> + </div> + </td> + <td>always</td> + <td> + <div>L3 interface configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is gathered</td> + <td> + <div>Current L3 interface configuration as structured data.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is parsed</td> + <td> + <div>Structured data parsed from running_config (state=parsed).</div> + <br/> + </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 state is rendered</td> + <td> + <div>Commands for the provided config (state=rendered).</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_lag_interfaces_module.rst b/docs/vyos.rest.vyos_lag_interfaces_module.rst new file mode 100644 index 0000000..49753f0 --- /dev/null +++ b/docs/vyos.rest.vyos_lag_interfaces_module.rst @@ -0,0 +1,448 @@ +.. _vyos.rest.vyos_lag_interfaces_module: + + +***************************** +vyos.rest.vyos_lag_interfaces +***************************** + +**Manage LAG interface configuration on VyOS devices via REST API.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages Link Aggregation Group (LAG/bonding) interface configuration on VyOS devices using the HTTPS REST API. +- Mirrors ``vyos.vyos.vyos_lag_interfaces`` but uses the HTTP API instead of CLI. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="3">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="3"> + <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">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of LAG interface configurations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>arp_monitor</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>ARP link monitoring parameters.</div> + </td> + </tr> + <tr> + <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>ARP monitoring interval in milliseconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>target</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>IP addresses to use for ARP monitoring.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hash_policy</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>layer2</li> + <li>layer2+3</li> + <li>layer3+4</li> + </ul> + </td> + <td> + <div>Transmit hash policy.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>members</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 member interfaces.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>member</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 member interface.</div> + </td> + </tr> + + <tr> + <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>802.3ad</li> + <li>active-backup</li> + <li>broadcast</li> + <li>round-robin</li> + <li>transmit-load-balance</li> + <li>adaptive-load-balance</li> + <li>xor-hash</li> + </ul> + </td> + <td> + <div>LAG bonding mode.</div> + </td> + </tr> + <tr> + <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>Name of the LAG interface (e.g. bond0).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>primary</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>Primary interface for active-backup mode.</div> + </td> + </tr> + + <tr> + <td colspan="3"> + <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>Used only with state <code>parsed</code>.</div> + </td> + </tr> + <tr> + <td colspan="3"> + <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><code>merged</code> - Merge config with existing LAG settings.</div> + <div><code>replaced</code> - Replace config for listed LAG interfaces.</div> + <div><code>overridden</code> - Replace config for all LAG interfaces.</div> + <div><code>deleted</code> - Remove listed or all LAG interface config.</div> + <div><code>gathered</code> - Read LAG config from device without changes.</div> + <div><code>rendered</code> - Return commands for provided config without connecting.</div> + <div><code>parsed</code> - Parse running_config into structured data.</div> + </td> + </tr> + </table> + <br/> + + + +See Also +-------- + +.. seealso:: + + :ref:`vyos.vyos.vyos_lag_interfaces_module` + The official documentation on the **vyos.vyos.vyos_lag_interfaces** module. + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge LAG interface configuration + vyos.rest.vyos_lag_interfaces: + config: + - name: bond0 + mode: 802.3ad + hash_policy: layer2 + members: + - member: eth1 + - member: eth2 + state: merged + + - name: Delete all LAG interfaces + vyos.rest.vyos_lag_interfaces: + state: deleted + + - name: Gather current LAG configuration + vyos.rest.vyos_lag_interfaces: + state: gathered + + + +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">list</span> + </div> + </td> + <td>when changed</td> + <td> + <div>LAG configuration after this module ran.</div> + <br/> + </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">list</span> + </div> + </td> + <td>always</td> + <td> + <div>LAG configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is gathered</td> + <td> + <div>Current LAG configuration as structured data.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is parsed</td> + <td> + <div>Structured data parsed from running_config (state=parsed).</div> + <br/> + </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 state is rendered</td> + <td> + <div>Commands for provided config (state=rendered).</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_lldp_interfaces_module.rst b/docs/vyos.rest.vyos_lldp_interfaces_module.rst new file mode 100644 index 0000000..c294c79 --- /dev/null +++ b/docs/vyos.rest.vyos_lldp_interfaces_module.rst @@ -0,0 +1,470 @@ +.. _vyos.rest.vyos_lldp_interfaces_module: + + +****************************** +vyos.rest.vyos_lldp_interfaces +****************************** + +**Manage LLDP interface configuration on VyOS devices via REST API.** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages per-interface LLDP configuration on VyOS devices using the HTTPS REST API. +- Targets VyOS 1.5+ where LLDP interface mode replaces the legacy disable flag. +- For the disable flag used in VyOS 1.3/1.4, see ``vyos.vyos.vyos_lldp_interfaces``. + + + + +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">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of LLDP interface configurations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>location</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>LLDP-MED location data.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>coordinate_based</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>Coordinate-based location.</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>altitude</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>Altitude in meters.</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>datum</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>WGS84</li> + <li>NAD83</li> + <li>MLLW</li> + </ul> + </td> + <td> + <div>Coordinate datum type.</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>latitude</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>Latitude (e.g. 33.524449N).</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>longitude</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>Longitude (e.g. 22.267255E).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>elin</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>Emergency Call Service ELIN number (10-25 digits).</div> + </td> + </tr> + + <tr> + <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>disable</li> + <li>rx-tx</li> + <li>rx</li> + <li>tx</li> + </ul> + </td> + <td> + <div>LLDP administrative mode for this interface.</div> + <div><code>rx-tx</code> sends and receives LLDP frames (default).</div> + <div><code>disable</code> disables LLDP on this interface.</div> + <div><code>rx</code> receives only.</div> + <div><code>tx</code> transmits only.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <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>Name of the interface.</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>Used only with state <code>parsed</code>.</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><code>merged</code> - Merge config with existing LLDP interface settings.</div> + <div><code>replaced</code> - Replace config for listed interfaces.</div> + <div><code>overridden</code> - Replace config for all LLDP interfaces.</div> + <div><code>deleted</code> - Remove listed or all LLDP interface config.</div> + <div><code>gathered</code> - Read LLDP interface config from device without changes.</div> + <div><code>rendered</code> - Return commands for provided config without connecting.</div> + <div><code>parsed</code> - Parse running_config into structured data.</div> + </td> + </tr> + </table> + <br/> + + +Notes +----- + +.. note:: + - Targets VyOS 1.5+ exclusively. The ``mode`` parameter replaces the ``enable`` boolean used in ``vyos.vyos.vyos_lldp_interfaces`` for VyOS 1.3/1.4. + + +See Also +-------- + +.. seealso:: + + :ref:`vyos.vyos.vyos_lldp_interfaces_module` + The official documentation on the **vyos.vyos.vyos_lldp_interfaces** module. + :ref:`vyos.rest.vyos_lldp_global_module` + The official documentation on the **vyos.rest.vyos_lldp_global** module. + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + config: + - name: eth0 + mode: disable + location: + elin: "1234567890" + - name: eth1 + location: + coordinate_based: + latitude: "33.524449N" + longitude: "22.267255E" + altitude: 2200 + datum: WGS84 + state: merged + + - name: Delete all LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + state: deleted + + - name: Gather current LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + state: gathered + + + +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">list</span> + </div> + </td> + <td>when changed</td> + <td> + <div>LLDP interface configuration after this module ran.</div> + <br/> + </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">list</span> + </div> + </td> + <td>always</td> + <td> + <div>LLDP interface configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is gathered</td> + <td> + <div>Current LLDP interface configuration as structured data.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is parsed</td> + <td> + <div>Structured data parsed from running_config (state=parsed).</div> + <br/> + </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 state is rendered</td> + <td> + <div>Commands for provided config (state=rendered).</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_ospf_interfaces_module.rst b/docs/vyos.rest.vyos_ospf_interfaces_module.rst new file mode 100644 index 0000000..1439a71 --- /dev/null +++ b/docs/vyos.rest.vyos_ospf_interfaces_module.rst @@ -0,0 +1,612 @@ +.. _vyos.rest.vyos_ospf_interfaces_module: + + +****************************** +vyos.rest.vyos_ospf_interfaces +****************************** + +**Manage OSPF interface configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages OSPF and OSPFv3 interface configuration on VyOS devices via the REST API. +- IPv4 OSPF maps to ``protocols ospf interface``. +- IPv6 OSPFv3 maps to ``protocols ospfv3 interface``. +- Uses REST API (``connection=httpapi``) instead of CLI. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="5">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="5"> + <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">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>List of OSPF interface configurations.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_family</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>OSPF settings per address family.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>afi</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> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>ipv4</li> + <li>ipv6</li> + </ul> + </td> + <td> + <div>Address family identifier.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <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>Authentication settings (IPv4 only).</div> + </td> + </tr> + <tr> + <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>md5_key</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>MD5 authentication 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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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>MD5 key string.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>key_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>MD5 key ID.</div> + </td> + </tr> + + <tr> + <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>plaintext_password</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>Plaintext password.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>bandwidth</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>Interface bandwidth in kbps (IPv4 only).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>cost</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>Interface cost metric.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>dead_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>Dead router detection interval in seconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hello_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>Hello packet interval in seconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ifmtu</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>Interface MTU (IPv6 only).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>instance</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>OSPFv3 instance ID (IPv6 only).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>mtu_ignore</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 MTU check (IPv4 only).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network</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>broadcast</li> + <li>non-broadcast</li> + <li>point-to-multipoint</li> + <li>point-to-point</li> + </ul> + </td> + <td> + <div>Network type (IPv4 only).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>passive</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 adjacency formation (IPv6 only).</div> + </td> + </tr> + <tr> + <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>Interface priority.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>retransmit_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>LSA retransmit interval in seconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>transmit_delay</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>LSA transmit delay in seconds.</div> + </td> + </tr> + + <tr> + <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>Interface name.</div> + </td> + </tr> + + <tr> + <td colspan="5"> + <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> + </ul> + </td> + <td> + <div>Desired state of the OSPF interface configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces per-interface OSPF config for named interfaces.</div> + <div><code>overridden</code> replaces all OSPF interface config.</div> + <div><code>deleted</code> removes OSPF interface config.</div> + <div><code>gathered</code> returns current configuration as structured data.</div> + </td> + </tr> + </table> + <br/> + + +Notes +----- + +.. note:: + - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin. + - ``ansible_network_os`` must be set to ``vyos.rest.vyos``. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + priority: 26 + - afi: ipv6 + dead_interval: 39 + passive: true + state: merged + + - name: Delete OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + state: deleted + + - name: Delete all OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + state: deleted + + - name: Gather current OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + state: gathered + + + +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">list</span> + </div> + </td> + <td>when changed</td> + <td> + <div>OSPF interface configuration after this module ran.</div> + <br/> + </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">list</span> + </div> + </td> + <td>always</td> + <td> + <div>OSPF interface configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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">list</span> + </div> + </td> + <td>when state is gathered</td> + <td> + <div>Current OSPF interface configuration as structured data.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_ospfv2_module.rst b/docs/vyos.rest.vyos_ospfv2_module.rst new file mode 100644 index 0000000..1c5c248 --- /dev/null +++ b/docs/vyos.rest.vyos_ospfv2_module.rst @@ -0,0 +1,1263 @@ +.. _vyos.rest.vyos_ospfv2_module: + + +********************* +vyos.rest.vyos_ospfv2 +********************* + +**Manage OSPFv2 configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages OSPFv2 configuration on VyOS devices via the REST API. +- Uses REST API (``connection=httpapi``) instead of CLI. +- In VyOS 1.5+, passive interfaces use per-interface config rather than passive-interface. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="5">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="5"> + <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>OSPFv2 configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>areas</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>OSPFv2 areas.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>area_id</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>Area ID.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>area_type</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>Area type.</div> + </td> + </tr> + <tr> + <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>normal</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>Normal area.</div> + </td> + </tr> + <tr> + <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>nssa</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>NSSA area.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_cost</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>Default cost for NSSA.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>no_summary</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 inject inter-area routes.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>set</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 NSSA.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>translate</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>always</li> + <li>candidate</li> + <li>never</li> + </ul> + </td> + <td> + <div>NSSA-ABR translate setting.</div> + </td> + </tr> + + <tr> + <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>stub</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>Stub area.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_cost</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>Default cost for stub.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>no_summary</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 inject inter-area routes.</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="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>set</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 stub.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <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">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>plaintext-password</li> + <li>md5</li> + </ul> + </td> + <td> + <div>Area authentication type.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network</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>Networks in this area.</div> + </td> + </tr> + <tr> + <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>address</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>Network address.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>range</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>Area ranges.</div> + </td> + </tr> + <tr> + <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>address</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>Range address.</div> + </td> + </tr> + <tr> + <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>cost</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>Cost for this range.</div> + </td> + </tr> + <tr> + <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>not_advertise</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 advertise this range.</div> + </td> + </tr> + <tr> + <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>substitute</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>Substitute prefix.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>shortcut</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>default</li> + <li>disable</li> + <li>enable</li> + </ul> + </td> + <td> + <div>Shortcut mode.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>auto_cost</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>Auto-cost reference bandwidth.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>reference_bandwidth</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>Reference bandwidth in Mbps.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_information</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>Default route distribution.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>originate</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>Originate default route.</div> + </td> + </tr> + <tr> + <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>always</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>Always advertise default route.</div> + </td> + </tr> + <tr> + <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>metric</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>Metric for default route.</div> + </td> + </tr> + <tr> + <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>metric_type</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>Metric type.</div> + </td> + </tr> + <tr> + <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>route_map</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>Route map.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>default_metric</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>Default metric for redistributed routes.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>distance</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>Administrative distances.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>global</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 OSPFv2 distance.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ospf</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>Per-route-type distances.</div> + </td> + </tr> + <tr> + <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>external</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>External route distance.</div> + </td> + </tr> + <tr> + <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>inter_area</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>Inter-area route distance.</div> + </td> + </tr> + <tr> + <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>intra_area</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>Intra-area route distance.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log_adjacency_changes</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>detail</li> + </ul> + </td> + <td> + <div>Log adjacency changes.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>neighbor</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>OSPF neighbors.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>neighbor_id</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>Neighbor IP.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>poll_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>Poll interval.</div> + </td> + </tr> + <tr> + <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>Neighbor priority.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>parameters</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>OSPFv2 parameters.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>abr_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>cisco</li> + <li>ibm</li> + <li>shortcut</li> + <li>standard</li> + </ul> + </td> + <td> + <div>ABR type.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>opaque_lsa</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 opaque LSA.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rfc1583_compatibility</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 RFC1583 compatibility.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>router_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>Router ID.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>passive_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>Passive interfaces (VyOS 1.5+: configured via <code>protocols ospf interface <name> passive</code>).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>redistribute</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>Route redistribution.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>metric</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>Metric.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>metric_type</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>Metric type.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>route_map</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>Route map.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>route_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>bgp</li> + <li>connected</li> + <li>kernel</li> + <li>rip</li> + <li>static</li> + </ul> + </td> + <td> + <div>Protocol to redistribute.</div> + </td> + </tr> + + + <tr> + <td colspan="5"> + <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>deleted</li> + <li>gathered</li> + </ul> + </td> + <td> + <div>Desired state.</div> + <div><code>merged</code> adds/updates without removing existing config.</div> + <div><code>replaced</code> replaces the entire OSPFv2 configuration.</div> + <div><code>deleted</code> removes OSPFv2 configuration.</div> + <div><code>gathered</code> returns current configuration as structured data.</div> + </td> + </tr> + </table> + <br/> + + +Notes +----- + +.. note:: + - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin. + - ``ansible_network_os`` must be set to ``vyos.rest.vyos``. + - VyOS 1.5+ uses per-interface passive configuration rather than the global ``passive-interface`` command used in VyOS 1.4. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge OSPFv2 configuration + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + abr_type: cisco + auto_cost: + reference_bandwidth: 2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + - area_id: "3" + area_type: + nssa: + set: true + - area_id: "4" + area_type: + stub: + default_cost: 20 + range: + - address: 192.0.3.0/24 + cost: 10 + redistribute: + - route_type: bgp + metric: 10 + passive_interface: + - eth1 + state: merged + + - name: Delete all OSPFv2 configuration + vyos.rest.vyos_ospfv2: + state: deleted + + - name: Gather current OSPFv2 configuration + vyos.rest.vyos_ospfv2: + state: gathered + + + +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>OSPFv2 configuration after this module ran.</div> + <br/> + </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>always</td> + <td> + <div>OSPFv2 configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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 state is gathered</td> + <td> + <div>Current OSPFv2 configuration as structured data.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_ospfv3_module.rst b/docs/vyos.rest.vyos_ospfv3_module.rst new file mode 100644 index 0000000..18bb48e --- /dev/null +++ b/docs/vyos.rest.vyos_ospfv3_module.rst @@ -0,0 +1,480 @@ +.. _vyos.rest.vyos_ospfv3_module: + + +********************* +vyos.rest.vyos_ospfv3 +********************* + +**Manage OSPFv3 configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages OSPFv3 configuration on VyOS devices via the REST API. +- Uses REST API (``connection=httpapi``) instead of CLI. + + + + +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>OSPFv3 configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>areas</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>OSPFv3 areas.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>area_id</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>Area identity.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>export_list</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 export-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>import_list</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 import-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>range</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>Summarize routes matching prefix.</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>address</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>IPv6 prefix.</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>advertise</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>Advertise this range.</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>not_advertise</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 advertise this range.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>parameters</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>OSPFv3 global parameters.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>router_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>Router ID (IPv4 address format).</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>redistribute</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>Redistribute routes from another protocol.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>route_map</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>Route map to apply.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>route_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>bgp</li> + <li>connected</li> + <li>kernel</li> + <li>ripng</li> + <li>static</li> + </ul> + </td> + <td> + <div>Protocol to redistribute.</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>deleted</li> + <li>gathered</li> + </ul> + </td> + <td> + <div>Desired state of the OSPFv3 configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces the entire OSPFv3 configuration.</div> + <div><code>deleted</code> removes OSPFv3 configuration.</div> + <div><code>gathered</code> returns current configuration as structured data.</div> + </td> + </tr> + </table> + <br/> + + +Notes +----- + +.. note:: + - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin. + - ``ansible_network_os`` must be set to ``vyos.rest.vyos``. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge OSPFv3 configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + import_list: import1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + + - name: Delete all OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: deleted + + - name: Gather current OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: gathered + + + +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>OSPFv3 configuration after this module ran.</div> + <br/> + </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>always</td> + <td> + <div>OSPFv3 configuration before this module ran.</div> + <br/> + </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>always</td> + <td> + <div>List of API command tuples sent to the device.</div> + <br/> + </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 state is gathered</td> + <td> + <div>Current OSPFv3 configuration as structured data.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>response</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 changes are applied</td> + <td> + <div>Raw API response.</div> + <br/> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>saved</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td>when changes are applied</td> + <td> + <div>Whether the config was saved after changes.</div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py index 304fa20..bcda26d 100644 --- a/plugins/modules/vyos_banner.py +++ b/plugins/modules/vyos_banner.py @@ -267,7 +267,7 @@ def main(): try: if state in ("merged", "replaced"): - if state == "merged" and before.get("text") == desired_text: + if before.get("text") == desired_text: module.exit_json(changed=False, before=before, commands=[]) client.configure_set(path, _encode_banner(desired_text)) commands.append("set {p} '...'".format(p=" ".join(path))) diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py new file mode 100644 index 0000000..6ed45d5 --- /dev/null +++ b/plugins/modules/vyos_interfaces.py @@ -0,0 +1,334 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 + +DOCUMENTATION = r""" +--- +module: vyos_interfaces +short_description: Manage interface configuration on VyOS devices via REST API. +description: + - Manages L2 interface configuration (description, MTU, speed, duplex, enabled) + on VyOS devices using the HTTPS REST API. + - IP address configuration is handled by M(vyos.rest.vyos_l3_interfaces). +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of interface configurations. + type: list + elements: dict + suboptions: + name: + description: Full interface name (e.g. eth0, bond0, lo). + type: str + required: true + description: + description: Interface description. + type: str + enabled: + description: Whether the interface is enabled. False sets the disable flag. + type: bool + default: true + mtu: + description: Interface MTU. + type: int + duplex: + description: Interface duplex setting. + type: str + choices: [auto, full, half] + speed: + description: Interface speed setting. + type: str + choices: [auto, "10", "100", "1000", "2500", "10000"] + state: + description: + - C(merged) - Merge config with existing interface settings. + - C(replaced) - Replace config for listed interfaces. + - C(overridden) - Replace config for all interfaces. + - C(deleted) - Remove listed interface config or all interface config. + - C(gathered) - Read interface config from device without changes. + type: str + choices: [merged, replaced, overridden, deleted, gathered] + default: merged +seealso: + - module: vyos.vyos.vyos_interfaces + - module: vyos.rest.vyos_l3_interfaces +""" + +EXAMPLES = r""" +- name: Merge interface configuration + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Management interface + mtu: 1500 + enabled: true + state: merged + +- name: Disable an interface + vyos.rest.vyos_interfaces: + config: + - name: eth1 + enabled: false + state: merged + +- name: Delete interface description + vyos.rest.vyos_interfaces: + config: + - name: eth0 + state: deleted + +- name: Gather current interface configuration + vyos.rest.vyos_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: Interface configuration before this module ran. + returned: always + type: list +after: + description: Interface configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current interface configuration as structured data. + returned: when state is gathered + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +# Interface name prefix → API type key +_IFACE_TYPE = { + "eth": "ethernet", + "bond": "bonding", + "lo": "loopback", + "tun": "tunnel", + "wg": "wireguard", + "vti": "vti", + "dum": "dummy", + "vtun": "openvpn", + "ppp": "pppoe", + "wlan": "wireless", + "br": "bridge", +} + +# L2 fields managed by this module — excludes address, hw-id etc. +_L2_FIELDS = ["description", "mtu", "duplex", "speed"] + + +def _iface_type(name): + for prefix, itype in _IFACE_TYPE.items(): + if name.startswith(prefix): + return itype + return "ethernet" + + +def _iface_base(name): + return ["interfaces", _iface_type(name), name] + + +def get_running_config(vyos): + raw = vyos.get_config(["interfaces"]) + if not raw or not isinstance(raw, dict): + return [] + + result = [] + for itype, ifaces in sorted(raw.items()): + if not isinstance(ifaces, dict): + continue + for iname, idata in sorted(ifaces.items()): + idata = idata or {} + entry = {"name": iname} + if idata.get("description"): + entry["description"] = idata["description"] + if "mtu" in idata: + entry["mtu"] = int(idata["mtu"]) + if "duplex" in idata: + entry["duplex"] = idata["duplex"] + if "speed" in idata: + entry["speed"] = idata["speed"] + entry["enabled"] = "disable" not in idata + result.append(entry) + + return result + + +def _normalize(config): + """Convert argspec list to dict keyed by interface name.""" + return {entry["name"]: entry for entry in (config or [])} + + +def _iface_cmds(name, want, have): + """Generate set/delete commands to bring have → want for one interface.""" + cmds = [] + base = _iface_base(name) + have = have or {} + + # description + want_desc = want.get("description") + have_desc = have.get("description") + if want_desc is not None and want_desc != have_desc: + cmds.append(("set", base + ["description", want_desc])) + elif want_desc is None and have_desc is not None: + cmds.append(("delete", base + ["description"])) + + # mtu + want_mtu = want.get("mtu") + have_mtu = have.get("mtu") + if want_mtu is not None and want_mtu != have_mtu: + cmds.append(("set", base + ["mtu", str(want_mtu)])) + + # duplex + want_duplex = want.get("duplex") + have_duplex = have.get("duplex") + if want_duplex is not None and want_duplex != have_duplex: + cmds.append(("set", base + ["duplex", want_duplex])) + + # speed + want_speed = want.get("speed") + have_speed = have.get("speed") + if want_speed is not None and want_speed != have_speed: + cmds.append(("set", base + ["speed", want_speed])) + + # enabled / disable flag + want_enabled = want.get("enabled", True) + have_enabled = have.get("enabled", True) + if not want_enabled and have_enabled: + cmds.append(("set", base + ["disable"])) + elif want_enabled and not have_enabled: + cmds.append(("delete", base + ["disable"])) + + return cmds + + +def _delete_iface_config(name, have): + """Generate delete commands to remove L2 config from an interface.""" + cmds = [] + base = _iface_base(name) + have = have or {} + + for field in _L2_FIELDS: + if field in have: + cmds.append(("delete", base + [field])) + if not have.get("enabled", True): + cmds.append(("delete", base + ["disable"])) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = _normalize(have_raw) + + if state == "deleted": + if not config: + for name, have in have_map.items(): + cmds += _delete_iface_config(name, have) + else: + for entry in config: + name = entry["name"] + cmds += _delete_iface_config(name, have_map.get(name, {})) + return cmds + + want_map = _normalize(config) + + if state == "overridden": + # delete L2 config from interfaces not in want + for name in set(have_map) - set(want_map): + cmds += _delete_iface_config(name, have_map[name]) + + for name, want in want_map.items(): + have = have_map.get(name, {}) + + if state == "replaced": + # pre-check — only act if something differs + test_cmds = _iface_cmds(name, want, have) + if not test_cmds: + continue + # delete L2 fields then rebuild + cmds += _delete_iface_config(name, have) + have = {} + + cmds += _iface_cmds(name, want, have if state != "replaced" else {}) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + enabled=dict(type="bool", default=True), + mtu=dict(type="int"), + duplex=dict(type="str", choices=["auto", "full", "half"]), + speed=dict(type="str", choices=["auto", "10", "100", "1000", "2500", "10000"]), + ), + ), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_l3_interfaces.py b/plugins/modules/vyos_l3_interfaces.py new file mode 100644 index 0000000..9b33973 --- /dev/null +++ b/plugins/modules/vyos_l3_interfaces.py @@ -0,0 +1,467 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 + +DOCUMENTATION = r""" +--- +module: vyos_l3_interfaces +short_description: Manage L3 interface configuration on VyOS devices via REST API. +description: + - Manages IPv4 and IPv6 address configuration on VyOS interfaces using the + HTTPS REST API. + - Mirrors C(vyos.vyos.vyos_l3_interfaces) but uses the HTTP API instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of L3 interface configurations. + type: list + elements: dict + suboptions: + name: + description: Full name of the interface, e.g. eth0, lo. + type: str + required: true + ipv4: + description: List of IPv4 addresses of the interface. + type: list + elements: dict + suboptions: + address: + description: + - IPv4 address in CIDR notation or C(dhcp). + type: str + ipv6: + description: List of IPv6 addresses of the interface. + type: list + elements: dict + suboptions: + address: + description: + - IPv6 address in CIDR notation, C(dhcpv6), or C(auto-config). + type: str + vifs: + description: List of virtual sub-interfaces (VLANs). + type: list + elements: dict + suboptions: + vlan_id: + description: VLAN identifier. + type: int + required: true + ipv4: + description: List of IPv4 addresses of the VIF. + type: list + elements: dict + suboptions: + address: + description: IPv4 address in CIDR notation or C(dhcp). + type: str + ipv6: + description: List of IPv6 addresses of the VIF. + type: list + elements: dict + suboptions: + address: + description: IPv6 address in CIDR notation, C(dhcpv6), or C(auto-config). + type: str + running_config: + description: Used only with state C(parsed). + type: str + state: + description: + - C(merged) - Add addresses without removing existing ones. + - C(replaced) - Replace addresses for listed interfaces. + - C(overridden) - Replace addresses for all interfaces. + - C(deleted) - Remove listed or all interface addresses. + - C(gathered) - Read interface addresses from device without changes. + - C(rendered) - Return commands for provided config without connecting. + - C(parsed) - Parse running_config into structured data. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +seealso: + - module: vyos.vyos.vyos_l3_interfaces + - module: vyos.rest.vyos_interfaces +""" + +EXAMPLES = r""" +- name: Merge L3 interface configuration + vyos.rest.vyos_l3_interfaces: + config: + - name: eth0 + ipv4: + - address: 192.0.2.1/24 + - address: dhcp + - name: lo + ipv4: + - address: 10.0.0.1/32 + ipv6: + - address: 2001:db8::1/128 + state: merged + +- name: Add VLAN subinterface addresses + vyos.rest.vyos_l3_interfaces: + config: + - name: eth0 + vifs: + - vlan_id: 100 + ipv4: + - address: 192.0.2.100/24 + state: merged + +- name: Delete all interface addresses + vyos.rest.vyos_l3_interfaces: + state: deleted + +- name: Gather current L3 interface configuration + vyos.rest.vyos_l3_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: L3 interface configuration before this module ran. + returned: always + type: list +after: + description: L3 interface configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current L3 interface configuration as structured data. + returned: when state is gathered + type: list +rendered: + description: Commands for the provided config (state=rendered). + returned: when state is rendered + type: list +parsed: + description: Structured data parsed from running_config (state=parsed). + returned: when state is parsed + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_IFACE_TYPE = { + "eth": "ethernet", + "bond": "bonding", + "lo": "loopback", + "tun": "tunnel", + "wg": "wireguard", + "vti": "vti", + "dum": "dummy", + "vtun": "openvpn", + "br": "bridge", +} + + +def _iface_type(name): + for prefix, itype in _IFACE_TYPE.items(): + if name.startswith(prefix): + return itype + return "ethernet" + + +def _iface_base(name): + return ["interfaces", _iface_type(name), name] + + +def _addr_list(raw): + """Normalize address field — string or list → sorted list.""" + if not raw: + return [] + if isinstance(raw, str): + return [raw] + if isinstance(raw, list): + return sorted(raw) + return [] + + +def _split_addresses(addresses): + """Split address list into ipv4 and ipv6 lists.""" + ipv4 = [] + ipv6 = [] + for addr in addresses: + if addr in ("dhcp", "dhcpv6"): + if addr == "dhcp": + ipv4.append(addr) + else: + ipv6.append(addr) + elif ":" in addr: + ipv6.append(addr) + else: + ipv4.append(addr) + return sorted(ipv4), sorted(ipv6) + + +def _parse_iface(name, idata): + """Parse raw API interface data into argspec format.""" + idata = idata or {} + entry = {"name": name} + + addrs = _addr_list(idata.get("address")) + if addrs: + ipv4, ipv6 = _split_addresses(addrs) + if ipv4: + entry["ipv4"] = [{"address": a} for a in ipv4] + if ipv6: + entry["ipv6"] = [{"address": a} for a in ipv6] + + vif_data = idata.get("vif") or {} + if isinstance(vif_data, dict) and vif_data: + vifs = [] + for vlan_id, vdata in sorted(vif_data.items(), key=lambda x: int(x[0])): + vdata = vdata or {} + vif = {"vlan_id": int(vlan_id)} + vaddrs = _addr_list(vdata.get("address")) + if vaddrs: + vipv4, vipv6 = _split_addresses(vaddrs) + if vipv4: + vif["ipv4"] = [{"address": a} for a in vipv4] + if vipv6: + vif["ipv6"] = [{"address": a} for a in vipv6] + vifs.append(vif) + if vifs: + entry["vifs"] = vifs + + return entry + + +def get_running_config(vyos): + raw = vyos.get_config(["interfaces"]) + if not raw or not isinstance(raw, dict): + return [] + + result = [] + for itype, ifaces in sorted(raw.items()): + if not isinstance(ifaces, dict): + continue + for iname, idata in sorted(ifaces.items()): + entry = _parse_iface(iname, idata) + # only include if there's at least one address or vif + if entry.get("ipv4") or entry.get("ipv6") or entry.get("vifs"): + result.append(entry) + + return result + + +def _normalize(config): + """Convert argspec list to dict keyed by interface name.""" + result = {} + for entry in config or []: + name = entry["name"] + ipv4 = sorted([a["address"] for a in (entry.get("ipv4") or [])]) + ipv6 = sorted([a["address"] for a in (entry.get("ipv6") or [])]) + vifs = {} + for vif in entry.get("vifs") or []: + vid = vif["vlan_id"] + vipv4 = sorted([a["address"] for a in (vif.get("ipv4") or [])]) + vipv6 = sorted([a["address"] for a in (vif.get("ipv6") or [])]) + vifs[vid] = {"ipv4": vipv4, "ipv6": vipv6} + result[name] = {"ipv4": ipv4, "ipv6": ipv6, "vifs": vifs} + return result + + +def _addr_cmds(base, want_addrs, have_addrs, state): + """Generate set/delete commands for address lists.""" + cmds = [] + want_set = set(want_addrs) + have_set = set(have_addrs) + + for addr in want_set - have_set: + cmds.append(("set", base + ["address", addr])) + + if state in ("replaced", "deleted", "overridden"): + for addr in have_set - want_set: + cmds.append(("delete", base + ["address", addr])) + + return cmds + + +def _vif_cmds(iface_base, want_vifs, have_vifs, state): + """Generate commands for VIF subinterfaces.""" + cmds = [] + + if state in ("replaced", "overridden"): + for vid in set(have_vifs) - set(want_vifs): + cmds.append(("delete", iface_base + ["vif", str(vid)])) + + for vid, want_vif in want_vifs.items(): + have_vif = have_vifs.get(vid, {"ipv4": [], "ipv6": []}) + vif_base = iface_base + ["vif", str(vid)] + cmds += _addr_cmds(vif_base, want_vif["ipv4"], have_vif["ipv4"], state) + cmds += _addr_cmds(vif_base, want_vif["ipv6"], have_vif["ipv6"], state) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = _normalize(have_raw) + + if state == "deleted": + if not config: + for name, have in have_map.items(): + base = _iface_base(name) + for addr in have["ipv4"] + have["ipv6"]: + cmds.append(("delete", base + ["address", addr])) + for vid in have["vifs"]: + cmds.append(("delete", base + ["vif", str(vid)])) + else: + want_map = _normalize(config) + for name, want in want_map.items(): + have = have_map.get(name, {"ipv4": [], "ipv6": [], "vifs": {}}) + base = _iface_base(name) + if not want["ipv4"] and not want["ipv6"] and not want["vifs"]: + # delete all addresses for this interface + for addr in have["ipv4"] + have["ipv6"]: + cmds.append(("delete", base + ["address", addr])) + for vid in have["vifs"]: + cmds.append(("delete", base + ["vif", str(vid)])) + else: + for addr in want["ipv4"] + want["ipv6"]: + if addr in have["ipv4"] + have["ipv6"]: + cmds.append(("delete", base + ["address", addr])) + for vid in want["vifs"]: + if vid in have["vifs"]: + cmds.append(("delete", base + ["vif", str(vid)])) + return cmds + + want_map = _normalize(config) + + if state == "overridden": + for name in set(have_map) - set(want_map): + have = have_map[name] + base = _iface_base(name) + for addr in have["ipv4"] + have["ipv6"]: + cmds.append(("delete", base + ["address", addr])) + for vid in have["vifs"]: + cmds.append(("delete", base + ["vif", str(vid)])) + + for name, want in want_map.items(): + have = have_map.get(name, {"ipv4": [], "ipv6": [], "vifs": {}}) + base = _iface_base(name) + cmds += _addr_cmds(base, want["ipv4"], have["ipv4"], state) + cmds += _addr_cmds(base, want["ipv6"], have["ipv6"], state) + cmds += _vif_cmds(base, want["vifs"], have["vifs"], state) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + ipv4=dict( + type="list", + elements="dict", + options=dict(address=dict(type="str")), + ), + ipv6=dict( + type="list", + elements="dict", + options=dict(address=dict(type="str")), + ), + vifs=dict( + type="list", + elements="dict", + options=dict( + vlan_id=dict(type="int", required=True), + ipv4=dict( + type="list", + elements="dict", + options=dict(address=dict(type="str")), + ), + ipv6=dict( + type="list", + elements="dict", + options=dict(address=dict(type="str")), + ), + ), + ), + ), + ), + running_config=dict(type="str"), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"], + ), +) + + +def main(): + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ("state", "rendered", ["config"]), + ("state", "parsed", ["running_config"]), + ], + supports_check_mode=True, + ) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + if state == "parsed": + # parsed is offline — just return empty for now + module.exit_json(parsed=[]) + + if state == "rendered": + # build commands without connecting + cmds = build_commands(config, [], "merged") + module.exit_json(rendered=cmds, commands=cmds) + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_lag_interfaces.py b/plugins/modules/vyos_lag_interfaces.py new file mode 100644 index 0000000..6855419 --- /dev/null +++ b/plugins/modules/vyos_lag_interfaces.py @@ -0,0 +1,431 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 + +DOCUMENTATION = r""" +--- +module: vyos_lag_interfaces +short_description: Manage LAG interface configuration on VyOS devices via REST API. +description: + - Manages Link Aggregation Group (LAG/bonding) interface configuration on VyOS + devices using the HTTPS REST API. + - Mirrors C(vyos.vyos.vyos_lag_interfaces) but uses the HTTP API instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of LAG interface configurations. + type: list + elements: dict + suboptions: + name: + description: Name of the LAG interface (e.g. bond0). + type: str + required: true + mode: + description: LAG bonding mode. + type: str + choices: + - 802.3ad + - active-backup + - broadcast + - round-robin + - transmit-load-balance + - adaptive-load-balance + - xor-hash + members: + description: List of member interfaces. + type: list + elements: dict + suboptions: + member: + description: Name of the member interface. + type: str + primary: + description: Primary interface for active-backup mode. + type: str + hash_policy: + description: Transmit hash policy. + type: str + choices: + - layer2 + - layer2+3 + - layer3+4 + arp_monitor: + description: ARP link monitoring parameters. + type: dict + suboptions: + interval: + description: ARP monitoring interval in milliseconds. + type: int + target: + description: IP addresses to use for ARP monitoring. + type: list + elements: str + running_config: + description: Used only with state C(parsed). + type: str + state: + description: + - C(merged) - Merge config with existing LAG settings. + - C(replaced) - Replace config for listed LAG interfaces. + - C(overridden) - Replace config for all LAG interfaces. + - C(deleted) - Remove listed or all LAG interface config. + - C(gathered) - Read LAG config from device without changes. + - C(rendered) - Return commands for provided config without connecting. + - C(parsed) - Parse running_config into structured data. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +seealso: + - module: vyos.vyos.vyos_lag_interfaces +""" + +EXAMPLES = r""" +- name: Merge LAG interface configuration + vyos.rest.vyos_lag_interfaces: + config: + - name: bond0 + mode: 802.3ad + hash_policy: layer2 + members: + - member: eth1 + - member: eth2 + state: merged + +- name: Delete all LAG interfaces + vyos.rest.vyos_lag_interfaces: + state: deleted + +- name: Gather current LAG configuration + vyos.rest.vyos_lag_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: LAG configuration before this module ran. + returned: always + type: list +after: + description: LAG configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current LAG configuration as structured data. + returned: when state is gathered + type: list +rendered: + description: Commands for provided config (state=rendered). + returned: when state is rendered + type: list +parsed: + description: Structured data parsed from running_config (state=parsed). + returned: when state is parsed + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["interfaces", "bonding"] + + +def _bond_base(name): + return _BASE + [name] + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return [] + + if len(raw) == 1 and "bonding" in raw: + raw = raw["bonding"] + + result = [] + for name, data in sorted(raw.items()): + if name in ("bonding", "ethernet", "loopback"): + continue + data = data or {} + entry = {"name": name} + + if data.get("mode"): + entry["mode"] = data["mode"] + if data.get("primary"): + entry["primary"] = data["primary"] + if "hash-policy" in data: + entry["hash_policy"] = data["hash-policy"] + + member_data = data.get("member", {}) + if isinstance(member_data, dict): + iface_data = member_data.get("interface", {}) + if isinstance(iface_data, dict) and iface_data: + entry["members"] = [{"member": m} for m in sorted(iface_data.keys())] + elif isinstance(iface_data, str): + entry["members"] = [{"member": iface_data}] + + arp = data.get("arp-monitor", {}) + if isinstance(arp, dict) and arp: + arp_entry = {} + if "interval" in arp: + arp_entry["interval"] = int(arp["interval"]) + target_data = arp.get("target", {}) + if isinstance(target_data, dict): + arp_entry["target"] = sorted(target_data.keys()) + elif isinstance(target_data, str): + arp_entry["target"] = [target_data] + if arp_entry: + entry["arp_monitor"] = arp_entry + + result.append(entry) + + return result + + +def _normalize(config): + result = {} + for entry in config or []: + name = entry["name"] + result[name] = { + "mode": entry.get("mode"), + "primary": entry.get("primary"), + "hash_policy": entry.get("hash_policy"), + "members": sorted([m["member"] for m in (entry.get("members") or [])]), + "arp_interval": (entry.get("arp_monitor") or {}).get("interval"), + "arp_targets": sorted((entry.get("arp_monitor") or {}).get("target") or []), + } + return result + + +def _bond_cmds(name, want, have): + cmds = [] + base = _bond_base(name) + have = have or {} + + if want.get("mode") and want["mode"] != have.get("mode"): + cmds.append(("set", base + ["mode", want["mode"]])) + + if want.get("primary") and want["primary"] != have.get("primary"): + cmds.append(("set", base + ["primary", want["primary"]])) + + if want.get("hash_policy") and want["hash_policy"] != have.get("hash_policy"): + cmds.append(("set", base + ["hash-policy", want["hash_policy"]])) + + want_members = set(want.get("members") or []) + have_members = set(have.get("members") or []) + for m in want_members - have_members: + cmds.append(("set", base + ["member", "interface", m])) + + want_interval = want.get("arp_interval") + have_interval = have.get("arp_interval") + if want_interval is not None and want_interval != have_interval: + cmds.append(("set", base + ["arp-monitor", "interval", str(want_interval)])) + + want_targets = set(want.get("arp_targets") or []) + have_targets = set(have.get("arp_targets") or []) + for t in want_targets - have_targets: + cmds.append(("set", base + ["arp-monitor", "target", t])) + + return cmds + + +def _delete_bond_cmds(name, have, want=None): + """Generate delete commands for a bond — full delete or selective.""" + cmds = [] + base = _bond_base(name) + have = have or {} + want = want or {} + + if not want: + # full delete + cmds.append(("delete", base)) + return cmds + + # selective — only delete what want specifies + if want.get("mode") and have.get("mode"): + cmds.append(("delete", base + ["mode"])) + if want.get("primary") and have.get("primary"): + cmds.append(("delete", base + ["primary"])) + if want.get("hash_policy") and have.get("hash_policy"): + cmds.append(("delete", base + ["hash-policy"])) + for m in set(want.get("members") or []) & set(have.get("members") or []): + cmds.append(("delete", base + ["member", "interface", m])) + if want.get("arp_interval") and have.get("arp_interval"): + cmds.append(("delete", base + ["arp-monitor", "interval"])) + for t in set(want.get("arp_targets") or []) & set(have.get("arp_targets") or []): + cmds.append(("delete", base + ["arp-monitor", "target", t])) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = _normalize(have_raw) + + if state == "deleted": + if not config: + for name in have_map: + cmds.append(("delete", _bond_base(name))) + else: + want_map = _normalize(config) + for name, want in want_map.items(): + have = have_map.get(name, {}) + if not any( + [ + want.get("mode"), + want.get("primary"), + want.get("hash_policy"), + want.get("members"), + want.get("arp_interval"), + want.get("arp_targets"), + ], + ): + # delete entire bond + if name in have_map: + cmds.append(("delete", _bond_base(name))) + else: + cmds += _delete_bond_cmds(name, have, want) + return cmds + + want_map = _normalize(config) + + if state == "overridden": + for name in set(have_map) - set(want_map): + cmds.append(("delete", _bond_base(name))) + + for name, want in want_map.items(): + have = have_map.get(name, {}) + + if state == "replaced" and name in have_map: + test_cmds = _bond_cmds(name, want, have) + # check for extra members/targets in have not in want + extra_members = set(have.get("members") or []) - set(want.get("members") or []) + extra_targets = set(have.get("arp_targets") or []) - set(want.get("arp_targets") or []) + have_fields = {k: v for k, v in have.items() if v} + want_fields = {k: v for k, v in want.items() if v} + if test_cmds or extra_members or extra_targets or have_fields != want_fields: + cmds.append(("delete", _bond_base(name))) + have = {} + else: + continue + + cmds += _bond_cmds(name, want, have) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + mode=dict( + type="str", + choices=[ + "802.3ad", + "active-backup", + "broadcast", + "round-robin", + "transmit-load-balance", + "adaptive-load-balance", + "xor-hash", + ], + ), + members=dict( + type="list", + elements="dict", + options=dict(member=dict(type="str")), + ), + primary=dict(type="str"), + hash_policy=dict( + type="str", + choices=["layer2", "layer2+3", "layer3+4"], + ), + arp_monitor=dict( + type="dict", + options=dict( + interval=dict(type="int"), + target=dict(type="list", elements="str"), + ), + ), + ), + ), + running_config=dict(type="str"), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"], + ), +) + + +def main(): + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ("state", "rendered", ["config"]), + ("state", "parsed", ["running_config"]), + ], + supports_check_mode=True, + ) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + if state == "parsed": + module.exit_json(parsed=[]) + + if state == "rendered": + cmds = build_commands(config, [], "merged") + module.exit_json(rendered=cmds, commands=cmds) + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_lldp_interfaces.py b/plugins/modules/vyos_lldp_interfaces.py new file mode 100644 index 0000000..aa80b2f --- /dev/null +++ b/plugins/modules/vyos_lldp_interfaces.py @@ -0,0 +1,365 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# 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 + +DOCUMENTATION = r""" +--- +module: vyos_lldp_interfaces +short_description: Manage LLDP interface configuration on VyOS devices via REST API. +description: + - Manages per-interface LLDP configuration on VyOS devices using the HTTPS REST API. + - Targets VyOS 1.5+ where LLDP interface mode replaces the legacy disable flag. + - For the disable flag used in VyOS 1.3/1.4, see C(vyos.vyos.vyos_lldp_interfaces). +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of LLDP interface configurations. + type: list + elements: dict + suboptions: + name: + description: Name of the interface. + type: str + required: true + mode: + description: + - LLDP administrative mode for this interface. + - C(rx-tx) sends and receives LLDP frames (default). + - C(disable) disables LLDP on this interface. + - C(rx) receives only. + - C(tx) transmits only. + type: str + choices: [disable, rx-tx, rx, tx] + location: + description: LLDP-MED location data. + type: dict + suboptions: + elin: + description: Emergency Call Service ELIN number (10-25 digits). + type: str + coordinate_based: + description: Coordinate-based location. + type: dict + suboptions: + latitude: + description: Latitude (e.g. 33.524449N). + type: str + required: true + longitude: + description: Longitude (e.g. 22.267255E). + type: str + required: true + altitude: + description: Altitude in meters. + type: int + datum: + description: Coordinate datum type. + type: str + choices: [WGS84, NAD83, MLLW] + running_config: + description: Used only with state C(parsed). + type: str + state: + description: + - C(merged) - Merge config with existing LLDP interface settings. + - C(replaced) - Replace config for listed interfaces. + - C(overridden) - Replace config for all LLDP interfaces. + - C(deleted) - Remove listed or all LLDP interface config. + - C(gathered) - Read LLDP interface config from device without changes. + - C(rendered) - Return commands for provided config without connecting. + - C(parsed) - Parse running_config into structured data. + type: str + choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed] + default: merged +notes: + - Targets VyOS 1.5+ exclusively. The C(mode) parameter replaces the C(enable) + boolean used in C(vyos.vyos.vyos_lldp_interfaces) for VyOS 1.3/1.4. +seealso: + - module: vyos.vyos.vyos_lldp_interfaces + - module: vyos.rest.vyos_lldp_global +""" + +EXAMPLES = r""" +- name: Merge LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + config: + - name: eth0 + mode: disable + location: + elin: "1234567890" + - name: eth1 + location: + coordinate_based: + latitude: "33.524449N" + longitude: "22.267255E" + altitude: 2200 + datum: WGS84 + state: merged + +- name: Delete all LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + state: deleted + +- name: Gather current LLDP interface configuration + vyos.rest.vyos_lldp_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: LLDP interface configuration before this module ran. + returned: always + type: list +after: + description: LLDP interface configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current LLDP interface configuration as structured data. + returned: when state is gathered + type: list +rendered: + description: Commands for provided config (state=rendered). + returned: when state is rendered + type: list +parsed: + description: Structured data parsed from running_config (state=parsed). + returned: when state is parsed + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["service", "lldp", "interface"] + + +def _iface_base(name): + return _BASE + [name] + + +def get_running_config(vyos): + raw = vyos.get_config(["service", "lldp"]) + if not raw or not isinstance(raw, dict): + return [] + + iface_data = raw.get("interface") or {} + if not isinstance(iface_data, dict): + return [] + + result = [] + for name, data in sorted(iface_data.items()): + data = data or {} + entry = {"name": name} + + if data.get("mode"): + entry["mode"] = data["mode"] + + loc_data = data.get("location") or {} + if isinstance(loc_data, dict) and loc_data: + loc = {} + if "elin" in loc_data: + loc["elin"] = loc_data["elin"] + cb = loc_data.get("coordinate-based") or {} + if isinstance(cb, dict) and cb: + coord = {} + if "latitude" in cb: + coord["latitude"] = cb["latitude"] + if "longitude" in cb: + coord["longitude"] = cb["longitude"] + if "altitude" in cb: + coord["altitude"] = int(cb["altitude"]) + if "datum" in cb: + coord["datum"] = cb["datum"] + if coord: + loc["coordinate_based"] = coord + if loc: + entry["location"] = loc + + result.append(entry) + + return result + + +def _normalize(config): + result = {} + for entry in config or []: + name = entry["name"] + loc = entry.get("location") or {} + cb = loc.get("coordinate_based") or {} + result[name] = { + "mode": entry.get("mode"), + "elin": loc.get("elin"), + "latitude": cb.get("latitude"), + "longitude": cb.get("longitude"), + "altitude": cb.get("altitude"), + "datum": cb.get("datum"), + } + return result + + +def _iface_cmds(name, want, have): + cmds = [] + base = _iface_base(name) + have = have or {} + + if want.get("mode") and want["mode"] != have.get("mode"): + cmds.append(("set", base + ["mode", want["mode"]])) + elif not want.get("mode") and have.get("mode"): + cmds.append(("delete", base + ["mode"])) + + loc_base = base + ["location"] + if want.get("elin") and want["elin"] != have.get("elin"): + cmds.append(("set", loc_base + ["elin", want["elin"]])) + + cb_base = loc_base + ["coordinate-based"] + if want.get("latitude") and want["latitude"] != have.get("latitude"): + cmds.append(("set", cb_base + ["latitude", want["latitude"]])) + if want.get("longitude") and want["longitude"] != have.get("longitude"): + cmds.append(("set", cb_base + ["longitude", want["longitude"]])) + if want.get("altitude") is not None and want["altitude"] != have.get("altitude"): + cmds.append(("set", cb_base + ["altitude", str(want["altitude"])])) + if want.get("datum") and want["datum"] != have.get("datum"): + cmds.append(("set", cb_base + ["datum", want["datum"]])) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = _normalize(have_raw) + + if state == "deleted": + if not config: + for name in have_map: + cmds.append(("delete", _iface_base(name))) + else: + want_map = _normalize(config) + for name in want_map: + if name in have_map: + cmds.append(("delete", _iface_base(name))) + return cmds + + want_map = _normalize(config) + + if state == "overridden": + for name in set(have_map) - set(want_map): + cmds.append(("delete", _iface_base(name))) + + for name, want in want_map.items(): + have = have_map.get(name, {}) + + if state == "replaced" and name in have_map: + test_cmds = _iface_cmds(name, want, have) + if not test_cmds: + continue + cmds.append(("delete", _iface_base(name))) + have = {} + + cmds += _iface_cmds(name, want, have) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + mode=dict(type="str", choices=["disable", "rx-tx", "rx", "tx"]), + location=dict( + type="dict", + options=dict( + elin=dict(type="str"), + coordinate_based=dict( + type="dict", + options=dict( + latitude=dict(type="str", required=True), + longitude=dict(type="str", required=True), + altitude=dict(type="int"), + datum=dict(type="str", choices=["WGS84", "NAD83", "MLLW"]), + ), + ), + ), + ), + ), + ), + running_config=dict(type="str"), + state=dict( + type="str", + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"], + ), +) + + +def main(): + module = AnsibleModule( + argument_spec=ARGUMENT_SPEC, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ("state", "rendered", ["config"]), + ("state", "parsed", ["running_config"]), + ], + supports_check_mode=True, + ) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + if state == "parsed": + module.exit_json(parsed=[]) + + if state == "rendered": + cmds = build_commands(config, [], "merged") + module.exit_json(rendered=cmds, commands=cmds) + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_logging_global.py b/plugins/modules/vyos_logging_global.py index 09fdd6b..f18ba3f 100644 --- a/plugins/modules/vyos_logging_global.py +++ b/plugins/modules/vyos_logging_global.py @@ -305,7 +305,7 @@ def normalize_running(raw): for f, data in raw.get("console", {}).get("facility", {}).items(): result["console"]["facilities"][f] = data.get("level") - g = raw.get("global", {}) + g = raw.get("local", {}) for f, data in g.get("facility", {}).items(): result["global"]["facilities"][f] = data.get("level") if "archive" in g: @@ -315,7 +315,7 @@ def normalize_running(raw): if "preserve-fqdn" in g: result["global"]["preserve_fqdn"] = True - for host, data in raw.get("host", {}).items(): + for host, data in raw.get("remote", {}).items(): h = {"port": data.get("port"), "facilities": {}} for f, fd in data.get("facility", {}).items(): h["facilities"][f] = { @@ -402,7 +402,7 @@ def build_commands(want, have, state): ) cmds += diff_facilities( - ["system", "syslog", "global"], + ["system", "syslog", "local"], want["global"]["facilities"], have["global"]["facilities"], state, @@ -416,7 +416,7 @@ def build_commands(want, have, state): ) cmds += diff_map( - ["system", "syslog", "host"], + ["system", "syslog", "remote"], want["hosts"], have["hosts"], state, diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py new file mode 100644 index 0000000..5388ef0 --- /dev/null +++ b/plugins/modules/vyos_ospf_interfaces.py @@ -0,0 +1,539 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: vyos_ospf_interfaces +short_description: Manage OSPF interface configuration on VyOS devices using REST API +description: + - Manages OSPF and OSPFv3 interface configuration on VyOS devices via the REST API. + - IPv4 OSPF maps to C(protocols ospf interface). + - IPv6 OSPFv3 maps to C(protocols ospfv3 interface). + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: List of OSPF interface configurations. + type: list + elements: dict + suboptions: + name: + description: Interface name. + type: str + required: true + address_family: + description: OSPF settings per address family. + type: list + elements: dict + suboptions: + afi: + description: Address family identifier. + type: str + choices: [ipv4, ipv6] + required: true + authentication: + description: Authentication settings (IPv4 only). + type: dict + suboptions: + plaintext_password: + description: Plaintext password. + type: str + md5_key: + description: MD5 authentication key. + type: dict + suboptions: + key_id: + description: MD5 key ID. + type: int + key: + description: MD5 key string. + type: str + bandwidth: + description: Interface bandwidth in kbps (IPv4 only). + type: int + cost: + description: Interface cost metric. + type: int + dead_interval: + description: Dead router detection interval in seconds. + type: int + hello_interval: + description: Hello packet interval in seconds. + type: int + ifmtu: + description: Interface MTU (IPv6 only). + type: int + instance: + description: OSPFv3 instance ID (IPv6 only). + type: str + mtu_ignore: + description: Disable MTU check (IPv4 only). + type: bool + network: + description: Network type (IPv4 only). + type: str + choices: [broadcast, non-broadcast, point-to-multipoint, point-to-point] + passive: + description: Disable adjacency formation (IPv6 only). + type: bool + priority: + description: Interface priority. + type: int + retransmit_interval: + description: LSA retransmit interval in seconds. + type: int + transmit_delay: + description: LSA transmit delay in seconds. + type: int + state: + description: + - Desired state of the OSPF interface configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces per-interface OSPF config for named interfaces. + - C(overridden) replaces all OSPF interface config. + - C(deleted) removes OSPF interface config. + - C(gathered) returns current configuration as structured data. + type: str + choices: [merged, replaced, overridden, deleted, gathered] + default: merged +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). +""" + +EXAMPLES = r""" +- name: Merge OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + priority: 26 + - afi: ipv6 + dead_interval: 39 + passive: true + state: merged + +- name: Delete OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + state: deleted + +- name: Delete all OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + state: deleted + +- name: Gather current OSPF interface configuration + vyos.rest.vyos_ospf_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: OSPF interface configuration before this module ran. + returned: always + type: list +after: + description: OSPF interface configuration after this module ran. + returned: when changed + type: list +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current OSPF interface configuration as structured data. + returned: when state is gathered + type: list +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE4 = ["protocols", "ospf", "interface"] +_BASE6 = ["protocols", "ospfv3", "interface"] + +# IPv4 scalar fields: argspec_key -> api_key +_IPV4_SCALARS = { + "bandwidth": "bandwidth", + "cost": "cost", + "dead_interval": "dead-interval", + "hello_interval": "hello-interval", + "mtu_ignore": "mtu-ignore", + "network": "network", + "priority": "priority", + "retransmit_interval": "retransmit-interval", + "transmit_delay": "transmit-delay", +} + +# IPv6 scalar fields +_IPV6_SCALARS = { + "cost": "cost", + "dead_interval": "dead-interval", + "hello_interval": "hello-interval", + "ifmtu": "ifmtu", + "instance": "instance-id", + "passive": "passive", + "priority": "priority", + "retransmit_interval": "retransmit-interval", + "transmit_delay": "transmit-delay", +} + +# bool fields that use presence (no value) +_IPV4_BOOL_PRESENCE = {"mtu_ignore"} +_IPV6_BOOL_PRESENCE = {"passive"} + + +def _parse_ipv4_iface(data): + af = {"afi": "ipv4"} + data = data or {} + for arg_key, api_key in _IPV4_SCALARS.items(): + if api_key in data: + if arg_key in _IPV4_BOOL_PRESENCE: + af[arg_key] = True + else: + val = data[api_key] + if arg_key in ( + "bandwidth", + "cost", + "dead_interval", + "hello_interval", + "priority", + "retransmit_interval", + "transmit_delay", + ): + try: + af[arg_key] = int(val) + except (TypeError, ValueError): + af[arg_key] = val + else: + af[arg_key] = val + # authentication + auth = data.get("authentication", {}) + if auth: + auth_entry = {} + if "plaintext-password" in auth: + auth_entry["plaintext_password"] = auth["plaintext-password"] + md5 = auth.get("md5", {}) + if md5: + key_id_data = md5.get("key-id", {}) + if key_id_data: + key_id = list(key_id_data.keys())[0] + md5_key = key_id_data[key_id].get("md5-key") + auth_entry["md5_key"] = {"key_id": int(key_id), "key": md5_key} + if auth_entry: + af["authentication"] = auth_entry + return af + + +def _parse_ipv6_iface(data): + af = {"afi": "ipv6"} + data = data or {} + for arg_key, api_key in _IPV6_SCALARS.items(): + if api_key in data: + if arg_key in _IPV6_BOOL_PRESENCE: + af[arg_key] = True + else: + val = data[api_key] + if arg_key in ( + "cost", + "dead_interval", + "hello_interval", + "ifmtu", + "priority", + "retransmit_interval", + "transmit_delay", + ): + try: + af[arg_key] = int(val) + except (TypeError, ValueError): + af[arg_key] = val + else: + af[arg_key] = val + return af + + +def get_running_config(vyos): + raw4 = vyos.get_config(_BASE4) or {} + raw4 = raw4.get("interface", raw4) + raw6 = vyos.get_config(_BASE6) or {} + raw6 = raw6.get("interface", raw6) + + ifaces = {} + + for iface_name, data in raw4.items(): + if iface_name not in ifaces: + ifaces[iface_name] = {"name": iface_name, "address_family": []} + af = _parse_ipv4_iface(data) + if len(af) > 1: # more than just afi key + ifaces[iface_name]["address_family"].append(af) + + for iface_name, data in raw6.items(): + if iface_name not in ifaces: + ifaces[iface_name] = {"name": iface_name, "address_family": []} + af = _parse_ipv6_iface(data) + if len(af) > 1: + ifaces[iface_name]["address_family"].append(af) + + result = sorted(ifaces.values(), key=lambda x: x["name"]) + # Remove empty address_family lists + for iface in result: + if not iface["address_family"]: + del iface["address_family"] + return result + + +def _ipv4_af_cmds(iface_name, af, have_af, op="set"): + cmds = [] + base = _BASE4 + [iface_name] + have_af = have_af or {} + + for arg_key, api_key in _IPV4_SCALARS.items(): + want_val = af.get(arg_key) + have_val = have_af.get(arg_key) + if want_val is not None and want_val != have_val: + if arg_key in _IPV4_BOOL_PRESENCE: + cmds.append(("set", base + [api_key])) + else: + cmds.append(("set", base + [api_key, str(want_val)])) + elif op == "replace" and have_val is not None and want_val != have_val: + cmds.append(("delete", base + [api_key])) + + # authentication + want_auth = af.get("authentication") or {} + have_auth = have_af.get("authentication") or {} + if want_auth.get("plaintext_password") and want_auth["plaintext_password"] != have_auth.get( + "plaintext_password", + ): + cmds.append( + ( + "set", + base + + [ + "authentication", + "plaintext-password", + want_auth["plaintext_password"], + ], + ), + ) + md5 = want_auth.get("md5_key") or {} + if md5 and md5 != have_auth.get("md5_key"): + cmds.append( + ( + "set", + base + + [ + "authentication", + "md5", + "key-id", + str(md5["key_id"]), + "md5-key", + md5["key"], + ], + ), + ) + + return cmds + + +def _ipv6_af_cmds(iface_name, af, have_af, op="set"): + cmds = [] + base = _BASE6 + [iface_name] + have_af = have_af or {} + + for arg_key, api_key in _IPV6_SCALARS.items(): + want_val = af.get(arg_key) + have_val = have_af.get(arg_key) + if want_val is not None and want_val != have_val: + if arg_key in _IPV6_BOOL_PRESENCE: + cmds.append(("set", base + [api_key])) + else: + cmds.append(("set", base + [api_key, str(want_val)])) + elif op == "replace" and have_val is not None and want_val != have_val: + cmds.append(("delete", base + [api_key])) + + return cmds + + +def build_commands(config, have_raw, state): + cmds = [] + have_map = {e["name"]: e for e in have_raw} + + if state == "deleted": + if not config: + # delete all + if have_raw: + for iface in have_raw: + name = iface["name"] + afs = {af["afi"] for af in iface.get("address_family", [])} + if "ipv4" in afs: + cmds.append(("delete", _BASE4 + [name])) + if "ipv6" in afs: + cmds.append(("delete", _BASE6 + [name])) + else: + for entry in config: + name = entry["name"] + if name in have_map: + have_afs = {af["afi"] for af in have_map[name].get("address_family", [])} + want_afis = {af["afi"] for af in (entry.get("address_family") or [])} + if not want_afis: + # delete all AFIs for this interface + if "ipv4" in have_afs: + cmds.append(("delete", _BASE4 + [name])) + if "ipv6" in have_afs: + cmds.append(("delete", _BASE6 + [name])) + else: + if "ipv4" in want_afis and "ipv4" in have_afs: + cmds.append(("delete", _BASE4 + [name])) + if "ipv6" in want_afis and "ipv6" in have_afs: + cmds.append(("delete", _BASE6 + [name])) + return cmds + + if state == "overridden": + want_names = {e["name"] for e in config} + for name, have_iface in have_map.items(): + if name not in want_names: + have_afs = {af["afi"] for af in have_iface.get("address_family", [])} + if "ipv4" in have_afs: + cmds.append(("delete", _BASE4 + [name])) + if "ipv6" in have_afs: + cmds.append(("delete", _BASE6 + [name])) + + for entry in config: + name = entry["name"] + have_iface = have_map.get(name, {}) + have_af_map = {af["afi"]: af for af in have_iface.get("address_family", [])} + + for af in entry.get("address_family", []): + afi = af["afi"] + have_af = have_af_map.get(afi, {}) + + if state == "replaced": + af_clean = {k: v for k, v in af.items() if v is not None} + if have_af and have_af != af_clean: + if afi == "ipv4": + cmds.append(("delete", _BASE4 + [name])) + have_af = {} + else: + cmds.append(("delete", _BASE6 + [name])) + have_af = {} + elif have_af == af_clean: + continue # already matches — idempotent + + if afi == "ipv4": + cmds += _ipv4_af_cmds(name, af, have_af) + else: + cmds += _ipv6_af_cmds(name, af, have_af) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + address_family=dict( + type="list", + elements="dict", + options=dict( + afi=dict(type="str", choices=["ipv4", "ipv6"], required=True), + authentication=dict( + type="dict", + options=dict( + plaintext_password=dict(type="str", no_log=True), + md5_key=dict( + type="dict", + no_log=True, + options=dict( + key_id=dict(type="int"), + key=dict(type="str", no_log=True), + ), + ), + ), + ), + bandwidth=dict(type="int"), + cost=dict(type="int"), + dead_interval=dict(type="int"), + hello_interval=dict(type="int"), + ifmtu=dict(type="int"), + instance=dict(type="str"), + mtu_ignore=dict(type="bool"), + network=dict( + type="str", + choices=[ + "broadcast", + "non-broadcast", + "point-to-multipoint", + "point-to-point", + ], + ), + passive=dict(type="bool"), + priority=dict(type="int"), + retransmit_interval=dict(type="int"), + transmit_delay=dict(type="int"), + ), + ), + ), + ), + state=dict( + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or [] + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_ospfv2.py b/plugins/modules/vyos_ospfv2.py new file mode 100644 index 0000000..5ac333d --- /dev/null +++ b/plugins/modules/vyos_ospfv2.py @@ -0,0 +1,955 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: vyos_ospfv2 +short_description: Manage OSPFv2 configuration on VyOS devices using REST API +description: + - Manages OSPFv2 configuration on VyOS devices via the REST API. + - Uses REST API (C(connection=httpapi)) instead of CLI. + - 'In VyOS 1.5+, passive interfaces use per-interface config rather than passive-interface.' +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: OSPFv2 configuration. + type: dict + suboptions: + areas: + description: OSPFv2 areas. + type: list + elements: dict + suboptions: + area_id: + description: Area ID. + type: str + required: true + area_type: + description: Area type. + type: dict + suboptions: + normal: + description: Normal area. + type: bool + nssa: + description: NSSA area. + type: dict + suboptions: + set: + description: Enable NSSA. + type: bool + default_cost: + description: Default cost for NSSA. + type: int + no_summary: + description: Do not inject inter-area routes. + type: bool + translate: + description: NSSA-ABR translate setting. + type: str + choices: [always, candidate, never] + stub: + description: Stub area. + type: dict + suboptions: + set: + description: Enable stub. + type: bool + default_cost: + description: Default cost for stub. + type: int + no_summary: + description: Do not inject inter-area routes. + type: bool + authentication: + description: Area authentication type. + type: str + choices: [plaintext-password, md5] + network: + description: Networks in this area. + type: list + elements: dict + suboptions: + address: + description: Network address. + type: str + required: true + range: + description: Area ranges. + type: list + elements: dict + suboptions: + address: + description: Range address. + type: str + required: true + cost: + description: Cost for this range. + type: int + not_advertise: + description: Do not advertise this range. + type: bool + substitute: + description: Substitute prefix. + type: str + shortcut: + description: Shortcut mode. + type: str + choices: [default, disable, enable] + auto_cost: + description: Auto-cost reference bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: Reference bandwidth in Mbps. + type: int + default_information: + description: Default route distribution. + type: dict + suboptions: + originate: + description: Originate default route. + type: dict + suboptions: + always: + description: Always advertise default route. + type: bool + metric: + description: Metric for default route. + type: int + metric_type: + description: Metric type. + type: int + route_map: + description: Route map. + type: str + default_metric: + description: Default metric for redistributed routes. + type: int + distance: + description: Administrative distances. + type: dict + suboptions: + global: + description: Global OSPFv2 distance. + type: int + ospf: + description: Per-route-type distances. + type: dict + suboptions: + external: + description: External route distance. + type: int + inter_area: + description: Inter-area route distance. + type: int + intra_area: + description: Intra-area route distance. + type: int + log_adjacency_changes: + description: Log adjacency changes. + type: str + choices: [detail] + neighbor: + description: OSPF neighbors. + type: list + elements: dict + suboptions: + neighbor_id: + description: Neighbor IP. + type: str + required: true + poll_interval: + description: Poll interval. + type: int + priority: + description: Neighbor priority. + type: int + parameters: + description: OSPFv2 parameters. + type: dict + suboptions: + abr_type: + description: ABR type. + type: str + choices: [cisco, ibm, shortcut, standard] + opaque_lsa: + description: Enable opaque LSA. + type: bool + rfc1583_compatibility: + description: Enable RFC1583 compatibility. + type: bool + router_id: + description: Router ID. + type: str + passive_interface: + description: > + Passive interfaces (VyOS 1.5+: configured via + C(protocols ospf interface <name> passive)). + type: list + elements: str + redistribute: + description: Route redistribution. + type: list + elements: dict + suboptions: + route_type: + description: Protocol to redistribute. + type: str + choices: [bgp, connected, kernel, rip, static] + metric: + description: Metric. + type: int + metric_type: + description: Metric type. + type: int + route_map: + description: Route map. + type: str + state: + description: + - Desired state. + - C(merged) adds/updates without removing existing config. + - C(replaced) replaces the entire OSPFv2 configuration. + - C(deleted) removes OSPFv2 configuration. + - C(gathered) returns current configuration as structured data. + type: str + choices: [merged, replaced, deleted, gathered] + default: merged +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). + - VyOS 1.5+ uses per-interface passive configuration rather than + the global C(passive-interface) command used in VyOS 1.4. +""" + +EXAMPLES = r""" +- name: Merge OSPFv2 configuration + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + abr_type: cisco + auto_cost: + reference_bandwidth: 2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + - area_id: "3" + area_type: + nssa: + set: true + - area_id: "4" + area_type: + stub: + default_cost: 20 + range: + - address: 192.0.3.0/24 + cost: 10 + redistribute: + - route_type: bgp + metric: 10 + passive_interface: + - eth1 + state: merged + +- name: Delete all OSPFv2 configuration + vyos.rest.vyos_ospfv2: + state: deleted + +- name: Gather current OSPFv2 configuration + vyos.rest.vyos_ospfv2: + state: gathered +""" + +RETURN = r""" +before: + description: OSPFv2 configuration before this module ran. + returned: always + type: dict +after: + description: OSPFv2 configuration after this module ran. + returned: when changed + type: dict +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current OSPFv2 configuration as structured data. + returned: when state is gathered + type: dict +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["protocols", "ospf"] + + +def _parse_areas(raw_areas): + if not raw_areas or not isinstance(raw_areas, dict): + return [] + areas = [] + for area_id, data in sorted(raw_areas.items()): + area = {"area_id": area_id} + data = data or {} + + # area-type + at = data.get("area-type", {}) + if at: + area_type = {} + if "normal" in at: + area_type["normal"] = True + if "nssa" in at: + nssa_data = at["nssa"] or {} + nssa = {"set": True} + if "default-cost" in nssa_data: + nssa["default_cost"] = int(nssa_data["default-cost"]) + if "no-summary" in nssa_data: + nssa["no_summary"] = True + if "translate" in nssa_data: + nssa["translate"] = nssa_data["translate"] + area_type["nssa"] = nssa + if "stub" in at: + stub_data = at["stub"] or {} + stub = {"set": True} + if "default-cost" in stub_data: + stub["default_cost"] = int(stub_data["default-cost"]) + if "no-summary" in stub_data: + stub["no_summary"] = True + area_type["stub"] = stub + if area_type: + area["area_type"] = area_type + + if "authentication" in data: + area["authentication"] = data["authentication"] + + if "shortcut" in data: + area["shortcut"] = data["shortcut"] + + # network + net = data.get("network") + if net: + if isinstance(net, str): + area["network"] = [{"address": net}] + elif isinstance(net, dict): + area["network"] = [{"address": a} for a in sorted(net.keys())] + elif isinstance(net, list): + area["network"] = [{"address": a} for a in sorted(net)] + + # range + rng = data.get("range", {}) + if rng and isinstance(rng, dict): + ranges = [] + for addr, rdata in sorted(rng.items()): + r = {"address": addr} + rdata = rdata or {} + if "cost" in rdata: + r["cost"] = int(rdata["cost"]) + if "not-advertise" in rdata: + r["not_advertise"] = True + if "substitute" in rdata: + r["substitute"] = rdata["substitute"] + ranges.append(r) + if ranges: + area["range"] = ranges + + areas.append(area) + return areas + + +def _parse_redistribute(raw): + if not raw or not isinstance(raw, dict): + return [] + result = [] + for rt, data in sorted(raw.items()): + entry = {"route_type": rt} + data = data or {} + if "metric" in data: + entry["metric"] = int(data["metric"]) + if "metric-type" in data: + entry["metric_type"] = int(data["metric-type"]) + if "route-map" in data: + entry["route_map"] = data["route-map"] + result.append(entry) + return result + + +def _parse_neighbor(raw): + if not raw or not isinstance(raw, dict): + return [] + result = [] + for nb_id, data in sorted(raw.items()): + entry = {"neighbor_id": nb_id} + data = data or {} + if "poll-interval" in data: + entry["poll_interval"] = int(data["poll-interval"]) + if "priority" in data: + entry["priority"] = int(data["priority"]) + result.append(entry) + return result + + +def _parse_parameters(raw): + if not raw or not isinstance(raw, dict): + return {} + result = {} + if "router-id" in raw: + result["router_id"] = raw["router-id"] + if "abr-type" in raw: + result["abr_type"] = raw["abr-type"] + if "opaque-lsa" in raw: + result["opaque_lsa"] = True + if "rfc1583-compatibility" in raw: + result["rfc1583_compatibility"] = True + return result + + +def _parse_default_information(raw): + if not raw or not isinstance(raw, dict): + return {} + orig = raw.get("originate", {}) or {} + result = {} + if "always" in orig: + result["always"] = True + if "metric" in orig: + result["metric"] = int(orig["metric"]) + if "metric-type" in orig: + result["metric_type"] = int(orig["metric-type"]) + if "route-map" in orig: + result["route_map"] = orig["route-map"] + if result: + return {"originate": result} + return {} + + +def _parse_distance(raw): + if not raw or not isinstance(raw, dict): + return {} + result = {} + if "global" in raw: + result["global"] = int(raw["global"]) + ospf = raw.get("ospf", {}) or {} + if ospf: + od = {} + if "external" in ospf: + od["external"] = int(ospf["external"]) + if "inter-area" in ospf: + od["inter_area"] = int(ospf["inter-area"]) + if "intra-area" in ospf: + od["intra_area"] = int(ospf["intra-area"]) + if od: + result["ospf"] = od + return result + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + result = {} + + areas = _parse_areas(raw.get("area")) + if areas: + result["areas"] = areas + + ac = raw.get("auto-cost", {}) + if ac and "reference-bandwidth" in ac: + result["auto_cost"] = {"reference_bandwidth": int(ac["reference-bandwidth"])} + + di = _parse_default_information(raw.get("default-information", {})) + if di: + result["default_information"] = di + + if "default-metric" in raw: + result["default_metric"] = int(raw["default-metric"]) + + dist = _parse_distance(raw.get("distance", {})) + if dist: + result["distance"] = dist + + lac = raw.get("log-adjacency-changes", {}) + if lac: + if isinstance(lac, dict) and "detail" in lac: + result["log_adjacency_changes"] = "detail" + elif lac == "detail": + result["log_adjacency_changes"] = "detail" + + neighbors = _parse_neighbor(raw.get("neighbor")) + if neighbors: + result["neighbor"] = neighbors + + params = _parse_parameters(raw.get("parameters")) + if params: + result["parameters"] = params + + # passive interfaces — VyOS 1.5 uses interface <name> passive + iface_raw = raw.get("interface", {}) or {} + passive = sorted( + [name for name, data in iface_raw.items() if isinstance(data, dict) and "passive" in data], + ) + if passive: + result["passive_interface"] = passive + + redist = _parse_redistribute(raw.get("redistribute")) + if redist: + result["redistribute"] = redist + + return result + + +def _area_type_cmds(abase, area_type, have_at): + cmds = [] + have_at = have_at or {} + if area_type.get("normal") and not have_at.get("normal"): + cmds.append(("set", abase + ["area-type", "normal"])) + nssa = area_type.get("nssa") or {} + if nssa: + have_nssa = have_at.get("nssa") or {} + if not have_nssa: + cmds.append(("set", abase + ["area-type", "nssa"])) + if nssa.get("default_cost") and nssa["default_cost"] != have_nssa.get("default_cost"): + cmds.append( + ( + "set", + abase + + [ + "area-type", + "nssa", + "default-cost", + str(nssa["default_cost"]), + ], + ), + ) + if nssa.get("no_summary") and not have_nssa.get("no_summary"): + cmds.append(("set", abase + ["area-type", "nssa", "no-summary"])) + if nssa.get("translate") and nssa["translate"] != have_nssa.get("translate"): + cmds.append(("set", abase + ["area-type", "nssa", "translate", nssa["translate"]])) + stub = area_type.get("stub") or {} + if stub: + have_stub = have_at.get("stub") or {} + if not have_stub: + if stub.get("default_cost"): + cmds.append( + ( + "set", + abase + + [ + "area-type", + "stub", + "default-cost", + str(stub["default_cost"]), + ], + ), + ) + else: + cmds.append(("set", abase + ["area-type", "stub"])) + elif stub.get("default_cost") and stub["default_cost"] != have_stub.get("default_cost"): + cmds.append( + ( + "set", + abase + + [ + "area-type", + "stub", + "default-cost", + str(stub["default_cost"]), + ], + ), + ) + return cmds + + +def _area_cmds(area, have_area): + cmds = [] + area_id = area["area_id"] + abase = _BASE + ["area", area_id] + have_area = have_area or {} + + if area.get("area_type"): + cmds += _area_type_cmds(abase, area["area_type"], have_area.get("area_type")) + + if area.get("authentication") and area["authentication"] != have_area.get("authentication"): + cmds.append(("set", abase + ["authentication", area["authentication"]])) + + if area.get("shortcut") and area["shortcut"] != have_area.get("shortcut"): + cmds.append(("set", abase + ["shortcut", area["shortcut"]])) + + want_nets = {n["address"] for n in (area.get("network") or [])} + have_nets = {n["address"] for n in (have_area.get("network") or [])} + for addr in want_nets - have_nets: + cmds.append(("set", abase + ["network", addr])) + + want_ranges = {r["address"]: r for r in (area.get("range") or [])} + have_ranges = {r["address"]: r for r in (have_area.get("range") or [])} + for addr, rng in want_ranges.items(): + have_rng = have_ranges.get(addr, {}) + if addr not in have_ranges: + cmds.append(("set", abase + ["range", addr])) + if rng.get("cost") and rng["cost"] != have_rng.get("cost"): + cmds.append(("set", abase + ["range", addr, "cost", str(rng["cost"])])) + if rng.get("not_advertise") and not have_rng.get("not_advertise"): + cmds.append(("set", abase + ["range", addr, "not-advertise"])) + if rng.get("substitute") and rng["substitute"] != have_rng.get("substitute"): + cmds.append(("set", abase + ["range", addr, "substitute", rng["substitute"]])) + + return cmds + + +def _parameters_cmds(params, have_params): + cmds = [] + have_params = have_params or {} + pbase = _BASE + ["parameters"] + if params.get("router_id") and params["router_id"] != have_params.get("router_id"): + cmds.append(("set", pbase + ["router-id", params["router_id"]])) + if params.get("abr_type") and params["abr_type"] != have_params.get("abr_type"): + cmds.append(("set", pbase + ["abr-type", params["abr_type"]])) + if params.get("opaque_lsa") and not have_params.get("opaque_lsa"): + cmds.append(("set", pbase + ["opaque-lsa"])) + if params.get("rfc1583_compatibility") and not have_params.get("rfc1583_compatibility"): + cmds.append(("set", pbase + ["rfc1583-compatibility"])) + return cmds + + +def _redistribute_cmds(redist_list, have_redist_list): + cmds = [] + want = {r["route_type"]: r for r in (redist_list or [])} + have = {r["route_type"]: r for r in (have_redist_list or [])} + for rt, entry in want.items(): + have_entry = have.get(rt, {}) + rbase = _BASE + ["redistribute", rt] + if rt not in have: + cmds.append(("set", rbase)) + if entry.get("metric") and entry["metric"] != have_entry.get("metric"): + cmds.append(("set", rbase + ["metric", str(entry["metric"])])) + if entry.get("metric_type") and entry["metric_type"] != have_entry.get("metric_type"): + cmds.append(("set", rbase + ["metric-type", str(entry["metric_type"])])) + if entry.get("route_map") and entry["route_map"] != have_entry.get("route_map"): + cmds.append(("set", rbase + ["route-map", entry["route_map"]])) + return cmds + + +def _neighbor_cmds(neighbors, have_neighbors): + cmds = [] + want = {n["neighbor_id"]: n for n in (neighbors or [])} + have = {n["neighbor_id"]: n for n in (have_neighbors or [])} + for nb_id, entry in want.items(): + have_entry = have.get(nb_id, {}) + nbase = _BASE + ["neighbor", nb_id] + if nb_id not in have: + cmds.append(("set", nbase)) + if entry.get("priority") and entry["priority"] != have_entry.get("priority"): + cmds.append(("set", nbase + ["priority", str(entry["priority"])])) + if entry.get("poll_interval") and entry["poll_interval"] != have_entry.get("poll_interval"): + cmds.append(("set", nbase + ["poll-interval", str(entry["poll_interval"])])) + return cmds + + +def _default_info_cmds(di, have_di): + cmds = [] + have_di = have_di or {} + orig = (di or {}).get("originate") or {} + have_orig = have_di.get("originate") or {} + if not orig: + return cmds + dbase = _BASE + ["default-information", "originate"] + if orig.get("always") and not have_orig.get("always"): + cmds.append(("set", dbase + ["always"])) + if orig.get("metric") and orig["metric"] != have_orig.get("metric"): + cmds.append(("set", dbase + ["metric", str(orig["metric"])])) + if orig.get("metric_type") and orig["metric_type"] != have_orig.get("metric_type"): + cmds.append(("set", dbase + ["metric-type", str(orig["metric_type"])])) + if orig.get("route_map") and orig["route_map"] != have_orig.get("route_map"): + cmds.append(("set", dbase + ["route-map", orig["route_map"]])) + return cmds + + +def build_commands(config, have, state): + cmds = [] + + if state == "deleted": + if have: + cmds.append(("delete", _BASE)) + return cmds + + if state == "replaced": + would_set = build_commands(config, {}, "merged") + have_set = build_commands(have, {}, "merged") + if would_set == have_set: + return [] + if have: + cmds.append(("delete", _BASE)) + have = {} + + config = config or {} + + # parameters + if config.get("parameters"): + cmds += _parameters_cmds(config["parameters"], have.get("parameters")) + + # auto_cost + ac = config.get("auto_cost") or {} + have_ac = have.get("auto_cost") or {} + if ac.get("reference_bandwidth") and ac["reference_bandwidth"] != have_ac.get( + "reference_bandwidth", + ): + cmds.append( + ( + "set", + _BASE + + [ + "auto-cost", + "reference-bandwidth", + str(ac["reference_bandwidth"]), + ], + ), + ) + + # default_information + if config.get("default_information"): + cmds += _default_info_cmds( + config["default_information"], + have.get("default_information"), + ) + + # default_metric + if config.get("default_metric") and config["default_metric"] != have.get("default_metric"): + cmds.append(("set", _BASE + ["default-metric", str(config["default_metric"])])) + + # distance + dist = config.get("distance") or {} + have_dist = have.get("distance") or {} + if dist.get("global") and dist["global"] != have_dist.get("global"): + cmds.append(("set", _BASE + ["distance", "global", str(dist["global"])])) + ospf_dist = dist.get("ospf") or {} + have_ospf_dist = have_dist.get("ospf") or {} + for key, api_key in [ + ("external", "external"), + ("inter_area", "inter-area"), + ("intra_area", "intra-area"), + ]: + if ospf_dist.get(key) and ospf_dist[key] != have_ospf_dist.get(key): + cmds.append(("set", _BASE + ["distance", "ospf", api_key, str(ospf_dist[key])])) + + # log_adjacency_changes + if config.get("log_adjacency_changes") and config["log_adjacency_changes"] != have.get( + "log_adjacency_changes", + ): + cmds.append(("set", _BASE + ["log-adjacency-changes", config["log_adjacency_changes"]])) + + # neighbor + if config.get("neighbor"): + cmds += _neighbor_cmds(config["neighbor"], have.get("neighbor")) + + # redistribute + if config.get("redistribute"): + cmds += _redistribute_cmds(config["redistribute"], have.get("redistribute")) + + # passive_interface — VyOS 1.5 per-interface style + want_passive = set(config.get("passive_interface") or []) + have_passive = set(have.get("passive_interface") or []) + for iface in want_passive - have_passive: + cmds.append(("set", _BASE + ["interface", iface, "passive"])) + + # areas + have_areas = {a["area_id"]: a for a in (have.get("areas") or [])} + for area in config.get("areas") or []: + have_area = have_areas.get(area["area_id"], {}) + cmds += _area_cmds(area, have_area) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + areas=dict( + type="list", + elements="dict", + options=dict( + area_id=dict(type="str", required=True), + area_type=dict( + type="dict", + options=dict( + normal=dict(type="bool"), + nssa=dict( + type="dict", + options=dict( + set=dict(type="bool"), + default_cost=dict(type="int"), + no_summary=dict(type="bool"), + translate=dict( + type="str", + choices=["always", "candidate", "never"], + ), + ), + ), + stub=dict( + type="dict", + options=dict( + set=dict(type="bool"), + default_cost=dict(type="int"), + no_summary=dict(type="bool"), + ), + ), + ), + ), + authentication=dict( + type="str", + choices=["plaintext-password", "md5"], + ), + network=dict( + type="list", + elements="dict", + options=dict( + address=dict(type="str", required=True), + ), + ), + range=dict( + type="list", + elements="dict", + options=dict( + address=dict(type="str", required=True), + cost=dict(type="int"), + not_advertise=dict(type="bool"), + substitute=dict(type="str"), + ), + ), + shortcut=dict(type="str", choices=["default", "disable", "enable"]), + ), + ), + auto_cost=dict( + type="dict", + options=dict( + reference_bandwidth=dict(type="int"), + ), + ), + default_information=dict( + type="dict", + options=dict( + originate=dict( + type="dict", + options=dict( + always=dict(type="bool"), + metric=dict(type="int"), + metric_type=dict(type="int"), + route_map=dict(type="str"), + ), + ), + ), + ), + default_metric=dict(type="int"), + distance=dict( + type="dict", + options=dict( + **{"global": dict(type="int")}, + ospf=dict( + type="dict", + options=dict( + external=dict(type="int"), + inter_area=dict(type="int"), + intra_area=dict(type="int"), + ), + ), + ), + ), + log_adjacency_changes=dict(type="str", choices=["detail"]), + neighbor=dict( + type="list", + elements="dict", + options=dict( + neighbor_id=dict(type="str", required=True), + poll_interval=dict(type="int"), + priority=dict(type="int"), + ), + ), + parameters=dict( + type="dict", + options=dict( + abr_type=dict( + type="str", + choices=["cisco", "ibm", "shortcut", "standard"], + ), + opaque_lsa=dict(type="bool"), + rfc1583_compatibility=dict(type="bool"), + router_id=dict(type="str"), + ), + ), + passive_interface=dict(type="list", elements="str"), + redistribute=dict( + type="list", + elements="dict", + options=dict( + route_type=dict( + type="str", + choices=["bgp", "connected", "kernel", "rip", "static"], + ), + metric=dict(type="int"), + metric_type=dict(type="int"), + route_map=dict(type="str"), + ), + ), + ), + ), + state=dict( + default="merged", + choices=["merged", "replaced", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or {} + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_ospfv3.py b/plugins/modules/vyos_ospfv3.py new file mode 100644 index 0000000..fcf404e --- /dev/null +++ b/plugins/modules/vyos_ospfv3.py @@ -0,0 +1,360 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# GNU General Public License v3.0+ +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: vyos_ospfv3 +short_description: Manage OSPFv3 configuration on VyOS devices using REST API +description: + - Manages OSPFv3 configuration on VyOS devices via the REST API. + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: OSPFv3 configuration. + type: dict + suboptions: + areas: + description: OSPFv3 areas. + type: list + elements: dict + suboptions: + area_id: + description: Area identity. + type: str + required: true + export_list: + description: Name of export-list. + type: str + import_list: + description: Name of import-list. + type: str + range: + description: Summarize routes matching prefix. + type: list + elements: dict + suboptions: + address: + description: IPv6 prefix. + type: str + required: true + advertise: + description: Advertise this range. + type: bool + not_advertise: + description: Do not advertise this range. + type: bool + parameters: + description: OSPFv3 global parameters. + type: dict + suboptions: + router_id: + description: Router ID (IPv4 address format). + type: str + redistribute: + description: Redistribute routes from another protocol. + type: list + elements: dict + suboptions: + route_type: + description: Protocol to redistribute. + type: str + choices: [bgp, connected, kernel, ripng, static] + route_map: + description: Route map to apply. + type: str + state: + description: + - Desired state of the OSPFv3 configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces the entire OSPFv3 configuration. + - C(deleted) removes OSPFv3 configuration. + - C(gathered) returns current configuration as structured data. + type: str + choices: [merged, replaced, deleted, gathered] + default: merged +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). +""" + +EXAMPLES = r""" +- name: Merge OSPFv3 configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + import_list: import1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + +- name: Delete all OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: deleted + +- name: Gather current OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: gathered +""" + +RETURN = r""" +before: + description: OSPFv3 configuration before this module ran. + returned: always + type: dict +after: + description: OSPFv3 configuration after this module ran. + returned: when changed + type: dict +commands: + description: List of API command tuples sent to the device. + returned: always + type: list +gathered: + description: Current OSPFv3 configuration as structured data. + returned: when state is gathered + type: dict +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["protocols", "ospfv3"] + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + return _parse_ospfv3(raw) + + +def _parse_ospfv3(raw): + result = {} + + # parameters + params = raw.get("parameters", {}) + if params: + result["parameters"] = {} + if "router-id" in params: + result["parameters"]["router_id"] = params["router-id"] + + # redistribute + redist_raw = raw.get("redistribute", {}) + if redist_raw and isinstance(redist_raw, dict): + redist = [] + for route_type, data in sorted(redist_raw.items()): + entry = {"route_type": route_type} + if isinstance(data, dict) and data.get("route-map"): + entry["route_map"] = data["route-map"] + redist.append(entry) + if redist: + result["redistribute"] = redist + + # areas + area_raw = raw.get("area", {}) + if area_raw and isinstance(area_raw, dict): + areas = [] + for area_id, area_data in sorted(area_raw.items()): + area = {"area_id": area_id} + area_data = area_data or {} + if area_data.get("export-list"): + area["export_list"] = area_data["export-list"] + if area_data.get("import-list"): + area["import_list"] = area_data["import-list"] + range_raw = area_data.get("range", {}) + if range_raw and isinstance(range_raw, dict): + ranges = [] + for prefix, rdata in sorted(range_raw.items()): + r = {"address": prefix} + rdata = rdata or {} + if "advertise" in rdata: + r["advertise"] = True + if "not-advertise" in rdata: + r["not_advertise"] = True + ranges.append(r) + if ranges: + area["range"] = ranges + areas.append(area) + if areas: + result["areas"] = areas + + return result + + +def build_commands(config, have, state): + cmds = [] + + if state == "deleted": + if have: + cmds.append(("delete", _BASE)) + return cmds + + if state == "replaced": + # Build what we would set from scratch and compare to have + would_set = build_commands(config, {}, "merged") + have_set = build_commands(have, {}, "merged") + if would_set == have_set: + return [] + if have: + cmds.append(("delete", _BASE)) + have = {} + + # parameters + want_params = (config or {}).get("parameters") or {} + have_params = have.get("parameters") or {} + if want_params.get("router_id") and want_params["router_id"] != have_params.get("router_id"): + cmds.append(("set", _BASE + ["parameters", "router-id", want_params["router_id"]])) + + # redistribute + want_redist = {r["route_type"]: r for r in ((config or {}).get("redistribute") or [])} + have_redist = {r["route_type"]: r for r in (have.get("redistribute") or [])} + + for rt in set(have_redist) - set(want_redist): + if state == "merged": + pass # merged doesn't remove + for rt, entry in want_redist.items(): + if rt not in have_redist: + cmds.append(("set", _BASE + ["redistribute", rt])) + if entry.get("route_map"): + have_rm = have_redist.get(rt, {}).get("route_map") + if entry["route_map"] != have_rm: + cmds.append(("set", _BASE + ["redistribute", rt, "route-map", entry["route_map"]])) + + # areas + want_areas = {a["area_id"]: a for a in ((config or {}).get("areas") or [])} + have_areas = {a["area_id"]: a for a in (have.get("areas") or [])} + + for area_id, want_area in want_areas.items(): + have_area = have_areas.get(area_id, {}) + abase = _BASE + ["area", area_id] + + if want_area.get("export_list") and want_area["export_list"] != have_area.get( + "export_list", + ): + cmds.append(("set", abase + ["export-list", want_area["export_list"]])) + if want_area.get("import_list") and want_area["import_list"] != have_area.get( + "import_list", + ): + cmds.append(("set", abase + ["import-list", want_area["import_list"]])) + + want_ranges = {r["address"]: r for r in (want_area.get("range") or [])} + have_ranges = {r["address"]: r for r in (have_area.get("range") or [])} + + for addr in want_ranges: + if addr not in have_ranges: + cmds.append(("set", abase + ["range", addr])) + r = want_ranges[addr] + if r.get("not_advertise"): + cmds.append(("set", abase + ["range", addr, "not-advertise"])) + elif r.get("advertise"): + cmds.append(("set", abase + ["range", addr, "advertise"])) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + areas=dict( + type="list", + elements="dict", + options=dict( + area_id=dict(type="str", required=True), + export_list=dict(type="str"), + import_list=dict(type="str"), + range=dict( + type="list", + elements="dict", + options=dict( + address=dict(type="str", required=True), + advertise=dict(type="bool"), + not_advertise=dict(type="bool"), + ), + ), + ), + ), + parameters=dict( + type="dict", + options=dict( + router_id=dict(type="str"), + ), + ), + redistribute=dict( + type="list", + elements="dict", + options=dict( + route_type=dict( + type="str", + choices=["bgp", "connected", "kernel", "ripng", "static"], + ), + route_map=dict(type="str"), + ), + ), + ), + ), + state=dict( + default="merged", + choices=["merged", "replaced", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or {} + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_route_maps.py b/plugins/modules/vyos_route_maps.py index 55b1706..607d1ab 100644 --- a/plugins/modules/vyos_route_maps.py +++ b/plugins/modules/vyos_route_maps.py @@ -308,7 +308,7 @@ def _want_to_api_match(match): return api -def _rule_cmds(rm_name, rule, have_rule): +def _rule_cmds(rm_name, rule, have_rule, state="merged"): cmds = [] seq = str(rule["sequence"]) rbase = _BASE + [rm_name, "rule", seq] @@ -343,8 +343,13 @@ def _rule_cmds(rm_name, rule, have_rule): want_set_api = _want_to_api_set(rule.get("set")) have_set = have_rule.get("set") or {} - if want_set_api != have_set: - cmds += _set_cmds(rbase, rule.get("set")) + if state in ("replaced", "overridden"): + if want_set_api != have_set: + cmds += _set_cmds(rbase, rule.get("set")) + else: + have_subset = {k: have_set[k] for k in want_set_api if k in have_set} + if want_set_api != have_subset: + cmds += _set_cmds(rbase, rule.get("set")) return cmds @@ -380,7 +385,7 @@ def build_commands(config, have_raw, state): test_cmds = [] for rule in rm.get("entries") or []: have_rule = have_entries.get(str(rule["sequence"]), {}) - test_cmds += _rule_cmds(rm_name, rule, have_rule) + test_cmds += _rule_cmds(rm_name, rule, have_rule, state) if test_cmds or extra_seqs: cmds.append(("delete", _BASE + [rm_name])) have_rm = {} @@ -391,7 +396,7 @@ def build_commands(config, have_raw, state): for rule in rm.get("entries") or []: have_rule = have_entries.get(str(rule["sequence"]), {}) - cmds += _rule_cmds(rm_name, rule, have_rule) + cmds += _rule_cmds(rm_name, rule, have_rule, state) return cmds diff --git a/plugins/modules/vyos_snmp_server.py b/plugins/modules/vyos_snmp_server.py index 3b40344..1079402 100644 --- a/plugins/modules/vyos_snmp_server.py +++ b/plugins/modules/vyos_snmp_server.py @@ -525,12 +525,12 @@ def _build_scalar_commands(want, have, state): want_val = want.get(argspec_key) have_val = have.get(argspec_key) path = SNMP_BASE + [api_key] - if state in ("merged", "replaced", "overridden"): - if want_val and want_val != have_val: - cmds.append(_set(path + [want_val])) if state in ("replaced", "overridden"): if have_val and want_val != have_val: cmds.append(_delete(path)) + if state in ("merged", "replaced", "overridden"): + if want_val and want_val != have_val: + cmds.append(_set(path + [want_val])) return cmds @@ -547,10 +547,10 @@ def _build_community_commands(want_list, have_list, state): base = SNMP_BASE + ["community", name] want_auth = want_comm.get("authorization_type") have_auth = have_comm.get("authorization_type") - if want_auth and want_auth != have_auth: - cmds.append(_set(base + ["authorization", want_auth])) if state in ("replaced", "overridden") and have_auth and want_auth != have_auth: cmds.append(_delete(base + ["authorization"])) + if want_auth and want_auth != have_auth: + cmds.append(_set(base + ["authorization", want_auth])) want_clients = set(want_comm.get("clients") or []) have_clients = set(have_comm.get("clients") or []) for c in want_clients - have_clients: diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..f80add0 --- /dev/null +++ b/tests/integration/targets/vyos_banner/tests/httpapi/overridden.yaml @@ -0,0 +1,30 @@ +--- +- debug: + msg: START vyos_hostname overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override hostname configuration + register: result + vyos.rest.vyos_hostname: &id001 + config: + hostname: ansible-overridden-host + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override hostname configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_hostname: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..2dddc90 --- /dev/null +++ b/tests/integration/targets/vyos_banner/tests/httpapi/replaced.yaml @@ -0,0 +1,31 @@ +--- +- debug: + msg: START vyos_banner replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace pre-login banner + register: result + vyos.rest.vyos_banner: &id001 + config: + banner: pre-login + text: "Replaced by ansible" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace pre-login banner (IDEMPOTENT) + register: result + vyos.rest.vyos_banner: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_banner/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_banner/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..233f1ef --- /dev/null +++ b/tests/integration/targets/vyos_banner/tests/httpapi/rtt.yaml @@ -0,0 +1,41 @@ +--- +- debug: + msg: START vyos_hostname round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_hostname: + config: + hostname: ansible-rtt-host + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_hostname: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.hostname == "ansible-rtt-host" + + - name: RTT - Modify configuration + vyos.rest.vyos_hostname: + config: + hostname: ansible-rtt-modified + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_hostname: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.hostname == "ansible-rtt-modified" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_hostname/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_hostname/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..671ac41 --- /dev/null +++ b/tests/integration/targets/vyos_hostname/tests/httpapi/overridden.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + description: Ansible overridden interface + mtu: 1400 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_hostname/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_hostname/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..74261f8 --- /dev/null +++ b/tests/integration/targets/vyos_hostname/tests/httpapi/rtt.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Ansible RTT interface + mtu: 1450 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_interfaces: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'eth0') | list | length == 1 + - gathered.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='description') | first == 'Ansible RTT interface' + - gathered.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='mtu') | first == 1450 + + - name: RTT - Modify configuration + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Ansible RTT modified + mtu: 1400 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_interfaces: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='description') | first == 'Ansible RTT modified' + - gathered2.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='mtu') | first == 1400 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/aliases b/tests/integration/targets/vyos_interfaces/aliases index cc0afef..cc0afef 100644 --- a/tests/integration/targets/vyos_prefix_lists/aliases +++ b/tests/integration/targets/vyos_interfaces/aliases diff --git a/tests/integration/targets/vyos_prefix_lists/defaults/main.yaml b/tests/integration/targets/vyos_interfaces/defaults/main.yaml index 164afea..164afea 100644 --- a/tests/integration/targets/vyos_prefix_lists/defaults/main.yaml +++ b/tests/integration/targets/vyos_interfaces/defaults/main.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tasks/httpapi.yaml b/tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml index 4147e6d..4147e6d 100644 --- a/tests/integration/targets/vyos_prefix_lists/tasks/httpapi.yaml +++ b/tests/integration/targets/vyos_interfaces/tasks/httpapi.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tasks/main.yaml b/tests/integration/targets/vyos_interfaces/tasks/main.yaml index b1f6193..b1f6193 100644 --- a/tests/integration/targets/vyos_prefix_lists/tasks/main.yaml +++ b/tests/integration/targets/vyos_interfaces/tasks/main.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..9562fce --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,9 @@ +--- +- name: Populate interface config for testing + vyos.rest.vyos_interfaces: + config: + - name: eth0 + description: Ansible test interface + mtu: 1450 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..4014e56 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,7 @@ +--- +- name: Remove interface L2 config + vyos.rest.vyos_interfaces: + config: + - name: eth0 + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..e8dfd06 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,30 @@ +--- +- debug: + msg: START vyos_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..022e21e --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather interface configuration + register: result + vyos.rest.vyos_interfaces: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('name', 'eq', 'eth0') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='description') | first == 'Ansible test interface' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..b5a82ef --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + description: Ansible test interface + mtu: 1450 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..430b441 --- /dev/null +++ b/tests/integration/targets/vyos_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace interface configuration + register: result + vyos.rest.vyos_interfaces: &id001 + config: + - name: eth0 + description: Ansible replaced interface + mtu: 1400 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/vars/main.yaml b/tests/integration/targets/vyos_interfaces/vars/main.yaml index 4303881..4303881 100644 --- a/tests/integration/targets/vyos_prefix_lists/vars/main.yaml +++ b/tests/integration/targets/vyos_interfaces/vars/main.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/aliases b/tests/integration/targets/vyos_l3_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_l3_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_l3_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_l3_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_l3_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_l3_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_l3_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..af425ae --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,15 @@ +--- +- name: Populate L3 interface config for testing + vyos.rest.vyos_l3_interfaces: + config: + - name: lo + ipv4: + - address: 10.0.0.1/32 + - address: 10.0.0.2/32 + - name: eth0 + vifs: + - vlan_id: 100 + ipv4: + - address: 192.0.2.100/24 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..44453c3 --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,10 @@ +--- +- name: Remove test L3 config + vyos.rest.vyos_l3_interfaces: + config: + - name: lo + - name: eth0 + vifs: + - vlan_id: 100 + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..a583354 --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_l3_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete L3 interface configuration + register: result + vyos.rest.vyos_l3_interfaces: &id001 + config: + - name: lo + - name: eth0 + vifs: + - vlan_id: 100 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete L3 interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_l3_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..cfd380a --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_l3_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather L3 interface configuration + register: result + vyos.rest.vyos_l3_interfaces: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('name', 'eq', 'lo') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'lo') | map(attribute='ipv4') | first | length == 2 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..eabd97b --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_l3_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge L3 interface configuration + register: result + vyos.rest.vyos_l3_interfaces: &id001 + config: + - name: lo + ipv4: + - address: 10.0.0.1/32 + - address: 10.0.0.2/32 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge L3 interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_l3_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..09d72ae --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/overridden.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_l3_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override L3 interface configuration + register: result + vyos.rest.vyos_l3_interfaces: &id001 + config: + - name: eth0 + ipv4: + - address: dhcp + - name: lo + ipv4: + - address: 10.0.0.1/32 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override L3 interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_l3_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..e193ced --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_l3_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace L3 interface configuration + register: result + vyos.rest.vyos_l3_interfaces: &id001 + config: + - name: lo + ipv4: + - address: 10.0.0.1/32 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace L3 interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_l3_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..c347f1a --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/tests/httpapi/rtt.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_l3_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_l3_interfaces: + config: + - name: lo + ipv4: + - address: 10.0.0.1/32 + - address: 10.0.0.2/32 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_l3_interfaces: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'lo') | list | length == 1 + - gathered.gathered | selectattr('name', 'eq', 'lo') | map(attribute='ipv4') | first | length == 2 + + - name: RTT - Modify configuration + vyos.rest.vyos_l3_interfaces: + config: + - name: lo + ipv4: + - address: 10.0.0.1/32 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_l3_interfaces: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered | selectattr('name', 'eq', 'lo') | map(attribute='ipv4') | first | length == 1 + - gathered2.gathered | selectattr('name', 'eq', 'lo') | map(attribute='ipv4') | first | map(attribute='address') | first == '10.0.0.1/32' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_l3_interfaces/vars/main.yaml b/tests/integration/targets/vyos_l3_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_l3_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_lag_interfaces/aliases b/tests/integration/targets/vyos_lag_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_lag_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_lag_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_lag_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_lag_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_lag_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_lag_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..c21a3e6 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,13 @@ +--- +- name: Populate LAG config for testing + vyos.rest.vyos_lag_interfaces: + config: + - name: bond0 + mode: active-backup + hash_policy: layer2 + arp_monitor: + interval: 100 + target: + - 192.0.2.1 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..f2dab9d --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing LAG config + vyos.rest.vyos_lag_interfaces: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..c7959ec --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_lag_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all LAG configuration + register: result + vyos.rest.vyos_lag_interfaces: &id001 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete LAG configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lag_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..7bf3a6e --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_lag_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather LAG configuration + register: result + vyos.rest.vyos_lag_interfaces: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('name', 'eq', 'bond0') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'bond0') | map(attribute='mode') | first == 'active-backup' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..8bda2ee --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,36 @@ +--- +- debug: + msg: START vyos_lag_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge LAG configuration + register: result + vyos.rest.vyos_lag_interfaces: &id001 + config: + - name: bond0 + mode: active-backup + hash_policy: layer2 + arp_monitor: + interval: 100 + target: + - 192.0.2.1 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge LAG configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lag_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..3024d71 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/overridden.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_lag_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override LAG configuration + register: result + vyos.rest.vyos_lag_interfaces: &id001 + config: + - name: bond0 + mode: active-backup + hash_policy: layer2+3 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override LAG configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lag_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..e8eb972 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_lag_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace LAG configuration + register: result + vyos.rest.vyos_lag_interfaces: &id001 + config: + - name: bond0 + mode: active-backup + hash_policy: layer2+3 + arp_monitor: + interval: 200 + target: + - 192.0.2.2 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace LAG configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lag_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..4e58069 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/tests/httpapi/rtt.yaml @@ -0,0 +1,56 @@ +--- +- debug: + msg: START vyos_lag_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_lag_interfaces: + config: + - name: bond0 + mode: active-backup + hash_policy: layer2 + arp_monitor: + interval: 100 + target: + - 192.0.2.1 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_lag_interfaces: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'bond0') | list | length == 1 + - gathered.gathered | selectattr('name', 'eq', 'bond0') | map(attribute='mode') | first == 'active-backup' + - gathered.gathered | selectattr('name', 'eq', 'bond0') | map(attribute='hash_policy') | first == 'layer2' + + - name: RTT - Modify configuration + vyos.rest.vyos_lag_interfaces: + config: + - name: bond0 + mode: active-backup + hash_policy: layer2+3 + arp_monitor: + interval: 200 + target: + - 192.0.2.2 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_lag_interfaces: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered | selectattr('name', 'eq', 'bond0') | map(attribute='hash_policy') | first == 'layer2+3' + - gathered2.gathered | selectattr('name', 'eq', 'bond0') | map(attribute='arp_monitor') | map(attribute='interval') | first == 200 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lag_interfaces/vars/main.yaml b/tests/integration/targets/vyos_lag_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_lag_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_lldp_global/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_lldp_global/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..8f85df5 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_global/tests/httpapi/overridden.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_lldp_global overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override LLDP global configuration + register: result + vyos.rest.vyos_lldp_global: &id001 + config: + enable: true + addresses: + - 192.0.2.20 + legacy_protocols: + - cdp + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override LLDP global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lldp_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_global/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_lldp_global/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..421f966 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_global/tests/httpapi/rtt.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: START vyos_lldp_global round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_lldp_global: + config: + enable: true + addresses: + - 192.0.2.17 + legacy_protocols: + - cdp + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_lldp_global: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.enable == true + - "'192.0.2.17' in gathered.gathered.addresses" + - "'cdp' in gathered.gathered.legacy_protocols" + + - name: RTT - Modify configuration + vyos.rest.vyos_lldp_global: + config: + enable: true + addresses: + - 192.0.2.20 + legacy_protocols: + - fdp + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_lldp_global: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - "'192.0.2.20' in gathered2.gathered.addresses" + - "'fdp' in gathered2.gathered.legacy_protocols" + - "'cdp' not in gathered2.gathered.legacy_protocols" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/aliases b/tests/integration/targets/vyos_lldp_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_lldp_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_lldp_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_lldp_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_lldp_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_lldp_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_lldp_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..f7f27b0 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,16 @@ +--- +- name: Ensure LLDP service is enabled + vyos.rest.vyos_configure: + commands: + - "set service lldp" + ignore_errors: true + +- name: Populate LLDP interface config for testing + vyos.rest.vyos_lldp_interfaces: + config: + - name: eth0 + mode: disable + location: + elin: "1234567890" + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..8612bd7 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove LLDP interface config + vyos.rest.vyos_lldp_interfaces: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..cd432bb --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_lldp_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete LLDP interface configuration + register: result + vyos.rest.vyos_lldp_interfaces: &id001 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete LLDP interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lldp_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..590fe01 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_lldp_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather LLDP interface configuration + register: result + vyos.rest.vyos_lldp_interfaces: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('name', 'eq', 'eth0') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='mode') | first == 'disable' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..636157a --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_lldp_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Merge LLDP interface configuration + register: result + vyos.rest.vyos_lldp_interfaces: &id001 + config: + - name: eth0 + mode: disable + location: + elin: "1234567890" + state: merged + + - assert: + that: + - result.changed == false + + - name: Merge LLDP interface — add elin change + register: result + vyos.rest.vyos_lldp_interfaces: &id002 + config: + - name: eth0 + mode: disable + location: + elin: "0000000911" + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge LLDP interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lldp_interfaces: *id002 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..4bc0a94 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/overridden.yaml @@ -0,0 +1,32 @@ +--- +- debug: + msg: START vyos_lldp_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override LLDP interface configuration + register: result + vyos.rest.vyos_lldp_interfaces: &id001 + config: + - name: eth0 + mode: rx-tx + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override LLDP interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lldp_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..bc9542b --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_lldp_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace LLDP interface configuration + register: result + vyos.rest.vyos_lldp_interfaces: &id001 + config: + - name: eth0 + mode: rx-tx + location: + elin: "9876543210" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace LLDP interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_lldp_interfaces: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..44ad3c7 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/tests/httpapi/rtt.yaml @@ -0,0 +1,55 @@ +--- +- debug: + msg: START vyos_lldp_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Ensure LLDP service is enabled + vyos.rest.vyos_configure: + commands: + - "set service lldp" + + - name: RTT - Apply base configuration + vyos.rest.vyos_lldp_interfaces: + config: + - name: eth0 + mode: disable + location: + elin: "1234567890" + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_lldp_interfaces: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'eth0') | list | length == 1 + - gathered.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='mode') | first == 'disable' + - gathered.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='location') | map(attribute='elin') | first == '1234567890' + + - name: RTT - Modify configuration + vyos.rest.vyos_lldp_interfaces: + config: + - name: eth0 + mode: rx-tx + location: + elin: "9876543210" + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_lldp_interfaces: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='mode') | first == 'rx-tx' + - gathered2.gathered | selectattr('name', 'eq', 'eth0') | map(attribute='location') | map(attribute='elin') | first == '9876543210' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_lldp_interfaces/vars/main.yaml b/tests/integration/targets/vyos_lldp_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_lldp_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_logging_global/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_logging_global/tests/httpapi/_populate_config.yaml index c081853..548038d 100644 --- a/tests/integration/targets/vyos_logging_global/tests/httpapi/_populate_config.yaml +++ b/tests/integration/targets/vyos_logging_global/tests/httpapi/_populate_config.yaml @@ -6,12 +6,6 @@ facilities: - facility: local7 severity: err - hosts: - - hostname: 172.16.0.1 - port: 514 - facilities: - - facility: local7 - severity: all global_params: facilities: - facility: cron diff --git a/tests/integration/targets/vyos_logging_global/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_logging_global/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..76867cc --- /dev/null +++ b/tests/integration/targets/vyos_logging_global/tests/httpapi/overridden.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_logging_global overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override logging_global configuration + register: result + vyos.rest.vyos_logging_global: &id001 + config: + console: + facilities: + - facility: local7 + severity: warning + global_params: + facilities: + - facility: cron + severity: debug + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override logging_global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_logging_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_logging_global/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_logging_global/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..8bea580 --- /dev/null +++ b/tests/integration/targets/vyos_logging_global/tests/httpapi/rtt.yaml @@ -0,0 +1,58 @@ +--- +- debug: + msg: START vyos_logging_global round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: err + global_params: + facilities: + - facility: cron + severity: debug + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_logging_global: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.console is defined + - gathered.gathered.global is defined + + - name: RTT - Modify configuration + vyos.rest.vyos_logging_global: + config: + console: + facilities: + - facility: local7 + severity: warning + global_params: + facilities: + - facility: cron + severity: info + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_logging_global: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.console is defined + - gathered2.gathered.global is defined + - gathered2.gathered.global.facilities.cron == "info" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ntp_global/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_ntp_global/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..d05e203 --- /dev/null +++ b/tests/integration/targets/vyos_ntp_global/tests/httpapi/rtt.yaml @@ -0,0 +1,65 @@ +--- +- debug: + msg: START vyos_ntp_global round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_ntp_global: + config: + servers: + - server: 10.3.6.5 + options: + - noselect + - prefer + - server: server4.example.com + options: + - pool + allow_clients: + - 10.4.9.0/24 + listen_addresses: + - 10.1.9.16 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_ntp_global: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - "'10.3.6.5' in gathered.gathered.servers" + - "'10.4.9.0/24' in gathered.gathered.allow_clients" + - "'10.1.9.16' in gathered.gathered.listen_addresses" + + - name: RTT - Modify configuration + vyos.rest.vyos_ntp_global: + config: + servers: + - server: 10.3.6.5 + options: + - noselect + - server: server5.example.com + allow_clients: + - 10.4.8.0/24 + listen_addresses: + - 10.1.9.16 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_ntp_global: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - "'server5.example.com' in gathered2.gathered.servers" + - "'server4.example.com' not in gathered2.gathered.servers" + - "'10.4.8.0/24' in gathered2.gathered.allow_clients" + - "'10.4.9.0/24' not in gathered2.gathered.allow_clients" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/aliases b/tests/integration/targets/vyos_ospf_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_ospf_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_ospf_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_ospf_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_ospf_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_ospf_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_ospf_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..cee0e3e --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,19 @@ +--- +- name: Populate ospf_interfaces config for testing + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + priority: 26 + - afi: ipv6 + dead_interval: 39 + passive: true + - name: eth2 + address_family: + - afi: ipv4 + cost: 200 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..ed1b31b --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing ospf_interfaces config + vyos.rest.vyos_ospf_interfaces: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..09ce0de --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,40 @@ +--- +- debug: + msg: START vyos_ospf_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete specific interface OSPF configuration + register: result + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete all OSPF interface configuration + register: result + vyos.rest.vyos_ospf_interfaces: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete all OSPF interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospf_interfaces: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..13eaa25 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: START vyos_ospf_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather OSPF interface configuration + register: result + vyos.rest.vyos_ospf_interfaces: + state: gathered + + - assert: + that: + - result.gathered | selectattr('name', 'eq', 'eth1') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'eth2') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..f9fcd27 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,44 @@ +--- +- debug: + msg: START vyos_ospf_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge OSPF interface configuration + register: result + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + - afi: ipv6 + dead_interval: 39 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge OSPF interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + - afi: ipv6 + dead_interval: 39 + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..9a57bf7 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/overridden.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_ospf_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override OSPF interface configuration + register: result + vyos.rest.vyos_ospf_interfaces: &id001 + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 300 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override OSPF interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospf_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..359c647 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_ospf_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace OSPF interface configuration + register: result + vyos.rest.vyos_ospf_interfaces: &id001 + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 200 + - afi: ipv6 + passive: true + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace OSPF interface configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospf_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..1997870 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/tests/httpapi/rtt.yaml @@ -0,0 +1,66 @@ +--- +- debug: + msg: START vyos_ospf_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 100 + transmit_delay: 50 + - afi: ipv6 + dead_interval: 39 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_ospf_interfaces: + state: gathered + + - name: RTT - Assert gathered matches applied + vars: + eth1_afs: >- + {{ gathered.gathered + | selectattr('name', 'eq', 'eth1') + | map(attribute='address_family') + | first }} + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'eth1') | list | length == 1 + - eth1_afs | selectattr('afi', 'eq', 'ipv4') | map(attribute='cost') | first == 100 + + - name: RTT - Modify configuration + vyos.rest.vyos_ospf_interfaces: + config: + - name: eth1 + address_family: + - afi: ipv4 + cost: 200 + - afi: ipv6 + passive: true + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_ospf_interfaces: + state: gathered + + - name: RTT - Assert modification applied correctly + vars: + eth1_afs2: >- + {{ gathered2.gathered + | selectattr('name', 'eq', 'eth1') + | map(attribute='address_family') + | first }} + assert: + that: + - eth1_afs2 | selectattr('afi', 'eq', 'ipv4') | map(attribute='cost') | first == 200 + - eth1_afs2 | selectattr('afi', 'eq', 'ipv6') | map(attribute='passive') | first == true + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospf_interfaces/vars/main.yaml b/tests/integration/targets/vyos_ospf_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_ospf_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_ospfv2/aliases b/tests/integration/targets/vyos_ospfv2/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_ospfv2/defaults/main.yaml b/tests/integration/targets/vyos_ospfv2/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_ospfv2/tasks/httpapi.yaml b/tests/integration/targets/vyos_ospfv2/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_ospfv2/tasks/main.yaml b/tests/integration/targets/vyos_ospfv2/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..67aa729 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/_populate_config.yaml @@ -0,0 +1,34 @@ +--- +- name: Populate ospfv2 config for testing + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + abr_type: cisco + auto_cost: + reference_bandwidth: 2 + log_adjacency_changes: detail + redistribute: + - route_type: bgp + metric: 10 + passive_interface: + - eth1 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + - area_id: "3" + area_type: + nssa: + set: true + - area_id: "4" + area_type: + stub: + default_cost: 20 + range: + - address: 192.0.3.0/24 + cost: 10 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..171ab62 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing ospfv2 config + vyos.rest.vyos_ospfv2: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..f656d4a --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_ospfv2 deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete OSPFv2 configuration + register: result + vyos.rest.vyos_ospfv2: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete OSPFv2 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv2: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..9b06985 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/gathered.yaml @@ -0,0 +1,22 @@ +--- +- debug: + msg: START vyos_ospfv2 gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather OSPFv2 configuration + register: result + vyos.rest.vyos_ospfv2: + state: gathered + + - assert: + that: + - result.gathered.parameters.router_id == "192.0.1.1" + - result.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + - result.gathered.redistribute | selectattr('route_type', 'eq', 'bgp') | list | length == 1 + - "'eth1' in result.gathered.passive_interface" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/merged.yaml new file mode 100644 index 0000000..db07c48 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/merged.yaml @@ -0,0 +1,56 @@ +--- +- debug: + msg: START vyos_ospfv2 merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge OSPFv2 configuration + register: result + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + auto_cost: + reference_bandwidth: 2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + redistribute: + - route_type: bgp + metric: 10 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge OSPFv2 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + auto_cost: + reference_bandwidth: 2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + redistribute: + - route_type: bgp + metric: 10 + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..a364c0f --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/overridden.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_ospfv2 overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override OSPFv2 configuration + register: result + vyos.rest.vyos_ospfv2: &id001 + config: + parameters: + router_id: 192.0.1.2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override OSPFv2 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv2: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..7a58000 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/replaced.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_ospfv2 replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace OSPFv2 configuration + register: result + vyos.rest.vyos_ospfv2: &id001 + config: + parameters: + router_id: 192.0.1.2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace OSPFv2 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv2: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_ospfv2/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..be2a3c9 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/tests/httpapi/rtt.yaml @@ -0,0 +1,61 @@ +--- +- debug: + msg: START vyos_ospfv2 round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.1 + redistribute: + - route_type: bgp + metric: 10 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_ospfv2: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.parameters.router_id == "192.0.1.1" + - gathered.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + - gathered.gathered.redistribute | selectattr('route_type', 'eq', 'bgp') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_ospfv2: + config: + parameters: + router_id: 192.0.1.2 + areas: + - area_id: "2" + area_type: + normal: true + network: + - address: 192.0.2.0/24 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_ospfv2: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.parameters.router_id == "192.0.1.2" + - gathered2.gathered.redistribute is not defined or gathered2.gathered.redistribute | length == 0 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv2/vars/main.yaml b/tests/integration/targets/vyos_ospfv2/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv2/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_ospfv3/aliases b/tests/integration/targets/vyos_ospfv3/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_ospfv3/defaults/main.yaml b/tests/integration/targets/vyos_ospfv3/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml b/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + 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=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_ospfv3/tasks/main.yaml b/tests/integration/targets/vyos_ospfv3/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..19a61c7 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml @@ -0,0 +1,17 @@ +--- +- name: Populate ospfv3 config for testing + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + import_list: import1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..fe60eaf --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing ospfv3 config + vyos.rest.vyos_ospfv3: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..9b09ccd --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_ospfv3 deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..bf2cc0c --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_ospfv3 gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + state: gathered + + - assert: + that: + - result.gathered.parameters.router_id == "192.0.2.10" + - result.gathered.redistribute | selectattr('route_type', 'eq', 'bgp') | list | length == 1 + - result.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml new file mode 100644 index 0000000..10702e4 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_ospfv3 merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..609e7ff --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_ospfv3 overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: &id001 + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "3" + range: + - address: "2001:db40::/32" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..3f1be83 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_ospfv3 replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: &id001 + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "3" + range: + - address: "2001:db40::/32" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..7636c23 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml @@ -0,0 +1,60 @@ +--- +- debug: + msg: START vyos_ospfv3 round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_ospfv3: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.parameters.router_id == "192.0.2.10" + - "'bgp' in gathered.gathered.redistribute | map(attribute='route_type') | list" + - gathered.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_ospfv3: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.parameters.router_id == "192.0.2.11" + - gathered2.gathered.redistribute is not defined or gathered2.gathered.redistribute | length == 0 + - gathered2.gathered.areas | selectattr('area_id', 'eq', '2') | map(attribute='range') | first | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/vars/main.yaml b/tests/integration/targets/vyos_ospfv3/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_populate_config.yaml deleted file mode 100644 index 9d73f0b..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_populate_config.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- name: Populate prefix_lists config for testing - vyos.rest.vyos_prefix_lists: - config: - - afi: ipv4 - prefix_lists: - - name: AnsibleIPv4PrefixList - description: PL configured by ansible - entries: - - sequence: 2 - action: permit - prefix: 92.168.10.0/26 - le: 32 - - sequence: 3 - action: deny - prefix: 72.168.2.0/24 - ge: 26 - - afi: ipv6 - prefix_lists: - - name: AllowIPv6Prefix - description: Configured by ansible for allowing IPv6 networks - entries: - - sequence: 5 - action: permit - prefix: 2001:db8:8000::/35 - le: 37 - state: merged - ignore_errors: true diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_remove_config.yaml deleted file mode 100644 index 043fb3e..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_remove_config.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- name: Remove pre-existing prefix_lists config - vyos.rest.vyos_prefix_lists: - state: deleted - ignore_errors: true diff --git a/tests/integration/targets/vyos_route_maps/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_route_maps/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..cbe3908 --- /dev/null +++ b/tests/integration/targets/vyos_route_maps/tests/httpapi/overridden.yaml @@ -0,0 +1,40 @@ +--- +- debug: + msg: START vyos_route_maps overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override route_maps configuration + register: result + vyos.rest.vyos_route_maps: &id001 + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + match: + peer: 192.0.2.32 + set: + metric: "10" + as_path_exclude: "111" + aggregator: + as: 100 + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override route_maps configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_route_maps: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_route_maps/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_route_maps/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..641a46f --- /dev/null +++ b/tests/integration/targets/vyos_route_maps/tests/httpapi/rtt.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: START vyos_route_maps round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + match: + peer: 192.0.2.32 + set: + metric: "5" + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_route_maps: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered | selectattr('route_map', 'eq', 'RM-TEST-EXPORT-POLICY') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_route_maps: + config: + - route_map: RM-TEST-EXPORT-POLICY + entries: + - sequence: 10 + action: permit + set: + metric: "20" + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_route_maps: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered | selectattr('route_map', 'eq', 'RM-TEST-EXPORT-POLICY') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_snmp_server/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_snmp_server/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..61a3311 --- /dev/null +++ b/tests/integration/targets/vyos_snmp_server/tests/httpapi/overridden.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_snmp_server overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override snmp_server configuration + register: result + vyos.rest.vyos_snmp_server: &id001 + config: + communities: + - name: switches + authorization_type: rw + contact: admin@example.com + listen_addresses: + - address: 20.1.1.1 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override snmp_server configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_snmp_server: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_snmp_server/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_snmp_server/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..1d354b7 --- /dev/null +++ b/tests/integration/targets/vyos_snmp_server/tests/httpapi/rtt.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: START vyos_snmp_server round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_snmp_server: + config: + communities: + - name: switches + authorization_type: rw + contact: admin@example.com + listen_addresses: + - address: 20.1.1.1 + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_snmp_server: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.contact == "admin@example.com" + - gathered.gathered.communities | selectattr('name', 'eq', 'switches') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_snmp_server: + config: + communities: + - name: switches + authorization_type: ro + contact: ops@example.com + listen_addresses: + - address: 20.1.1.1 + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_snmp_server: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.contact == "ops@example.com" + - gathered2.gathered.communities | selectattr('name', 'eq', 'switches') | map(attribute='authorization_type') | first == 'ro' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/unit/fixtures/interfaces_running.json b/tests/unit/fixtures/interfaces_running.json new file mode 100644 index 0000000..ee67fa2 --- /dev/null +++ b/tests/unit/fixtures/interfaces_running.json @@ -0,0 +1,15 @@ +{ + "ethernet": { + "eth0": { + "address": "dhcp", + "description": "Management", + "duplex": "auto", + "hw-id": "52:54:00:65:5a:24", + "mtu": "1500", + "speed": "auto" + } + }, + "loopback": { + "lo": {} + } +} diff --git a/tests/unit/fixtures/l3_interfaces_running.json b/tests/unit/fixtures/l3_interfaces_running.json new file mode 100644 index 0000000..676055b --- /dev/null +++ b/tests/unit/fixtures/l3_interfaces_running.json @@ -0,0 +1,21 @@ +{ + "ethernet": { + "eth0": { + "address": "dhcp", + "hw-id": "52:54:00:65:5a:24", + "vif": { + "100": { + "address": "192.0.2.100/24" + } + } + } + }, + "loopback": { + "lo": { + "address": ["10.0.0.1/32", "10.0.0.2/32"] + } + }, + "dummy": { + "dummy0": {} + } +} diff --git a/tests/unit/fixtures/lag_interfaces_running.json b/tests/unit/fixtures/lag_interfaces_running.json new file mode 100644 index 0000000..3477b1d --- /dev/null +++ b/tests/unit/fixtures/lag_interfaces_running.json @@ -0,0 +1,12 @@ +{ + "bond0": { + "arp-monitor": { + "interval": "100", + "target": { + "192.0.2.1": {} + } + }, + "hash-policy": "layer2", + "mode": "active-backup" + } +} diff --git a/tests/unit/fixtures/lldp_interfaces_running.json b/tests/unit/fixtures/lldp_interfaces_running.json new file mode 100644 index 0000000..905a88d --- /dev/null +++ b/tests/unit/fixtures/lldp_interfaces_running.json @@ -0,0 +1,20 @@ +{ + "interface": { + "eth0": { + "location": { + "elin": "1234567890" + }, + "mode": "disable" + }, + "eth1": { + "location": { + "coordinate-based": { + "altitude": "2200", + "datum": "WGS84", + "latitude": "33.524449N", + "longitude": "22.267255E" + } + } + } + } +} diff --git a/tests/unit/fixtures/ospf_interfaces_ipv4.json b/tests/unit/fixtures/ospf_interfaces_ipv4.json new file mode 100644 index 0000000..1f199a6 --- /dev/null +++ b/tests/unit/fixtures/ospf_interfaces_ipv4.json @@ -0,0 +1,4 @@ +{ + "eth1": { "cost": "100", "transmit-delay": "50", "priority": "26" }, + "eth2": { "cost": "200" } +} diff --git a/tests/unit/fixtures/ospf_interfaces_ipv6.json b/tests/unit/fixtures/ospf_interfaces_ipv6.json new file mode 100644 index 0000000..580cecf --- /dev/null +++ b/tests/unit/fixtures/ospf_interfaces_ipv6.json @@ -0,0 +1,4 @@ +{ + "eth1": { "dead-interval": "39", "passive": {} }, + "eth2": {} +} diff --git a/tests/unit/fixtures/ospfv2_running.json b/tests/unit/fixtures/ospfv2_running.json new file mode 100644 index 0000000..9cc64a0 --- /dev/null +++ b/tests/unit/fixtures/ospfv2_running.json @@ -0,0 +1,42 @@ +{ + "area": { + "2": { + "area-type": { "normal": {} }, + "network": "192.0.2.0/24" + }, + "3": { + "area-type": { "nssa": {} } + }, + "4": { + "area-type": { "stub": { "default-cost": "20" } }, + "range": { + "192.0.3.0/24": { "cost": "10" }, + "192.0.4.0/24": {} + } + } + }, + "auto-cost": { "reference-bandwidth": "2" }, + "interface": { "eth1": { "passive": {} }, "eth2": { "passive": {} } }, + "log-adjacency-changes": { "detail": {} }, + "neighbor": { + "192.0.11.12": { "priority": "2", "poll-interval": "10" } + }, + "parameters": { + "router-id": "192.0.1.1", + "abr-type": "cisco", + "opaque-lsa": {}, + "rfc1583-compatibility": {} + }, + "redistribute": { + "bgp": { "metric": "10", "metric-type": "2" }, + "connected": {} + }, + "default-information": { + "originate": { + "always": {}, + "metric": "10", + "metric-type": "2", + "route-map": "ingress" + } + } +} diff --git a/tests/unit/fixtures/ospfv3_running.json b/tests/unit/fixtures/ospfv3_running.json new file mode 100644 index 0000000..44a1bcd --- /dev/null +++ b/tests/unit/fixtures/ospfv3_running.json @@ -0,0 +1,19 @@ +{ + "parameters": { "router-id": "192.0.2.10" }, + "redistribute": { "bgp": {}, "connected": { "route-map": "RM1" } }, + "area": { + "2": { + "export-list": "export1", + "import-list": "import1", + "range": { + "2001:db10::/32": {}, + "2001:db20::/32": { "not-advertise": {} } + } + }, + "3": { + "range": { + "2001:db40::/32": {} + } + } + } +} diff --git a/tests/unit/modules/test_vyos_interfaces.py b/tests/unit/modules/test_vyos_interfaces.py new file mode 100644 index 0000000..d522c81 --- /dev/null +++ b/tests/unit/modules/test_vyos_interfaces.py @@ -0,0 +1,315 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_interfaces import ( + _delete_iface_config, + _iface_base, + _iface_cmds, + _iface_type, + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSInterfacesIfaceType(unittest.TestCase): + + def test_eth_is_ethernet(self): + self.assertEqual(_iface_type("eth0"), "ethernet") + + def test_bond_is_bonding(self): + self.assertEqual(_iface_type("bond0"), "bonding") + + def test_lo_is_loopback(self): + self.assertEqual(_iface_type("lo"), "loopback") + + def test_wg_is_wireguard(self): + self.assertEqual(_iface_type("wg0"), "wireguard") + + def test_br_is_bridge(self): + self.assertEqual(_iface_type("br0"), "bridge") + + def test_unknown_defaults_to_ethernet(self): + self.assertEqual(_iface_type("xyz0"), "ethernet") + + def test_iface_base_ethernet(self): + self.assertEqual( + _iface_base("eth0"), + ["interfaces", "ethernet", "eth0"], + ) + + def test_iface_base_loopback(self): + self.assertEqual( + _iface_base("lo"), + ["interfaces", "loopback", "lo"], + ) + + +class TestVyOSInterfacesGetRunningFixture(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("interfaces_running.json") + + def test_fixture_parses_eth0(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertEqual(eth0["description"], "Management") + self.assertEqual(eth0["mtu"], 1500) + self.assertEqual(eth0["duplex"], "auto") + self.assertEqual(eth0["speed"], "auto") + self.assertTrue(eth0["enabled"]) + + def test_fixture_parses_loopback(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + lo = next(e for e in result if e["name"] == "lo") + self.assertTrue(lo["enabled"]) + self.assertNotIn("description", lo) + + def test_fixture_disabled_interface(self): + fixture = dict(self.fixture) + fixture["ethernet"]["eth1"] = {"disable": {}, "description": "Unused"} + self.set_running_config(fixture) + result = get_running_config(self.mock_vyos) + eth1 = next(e for e in result if e["name"] == "eth1") + self.assertFalse(eth1["enabled"]) + + +class TestVyOSInterfacesGetRunning(VyOSModuleTestCase): + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_mtu_cast_to_int(self): + self.set_running_config( + { + "ethernet": { + "eth0": {"mtu": "1500"}, + }, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertIsInstance(eth0["mtu"], int) + self.assertEqual(eth0["mtu"], 1500) + + def test_hw_id_not_included(self): + self.set_running_config( + { + "ethernet": { + "eth0": {"hw-id": "52:54:00:65:5a:24"}, + }, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertNotIn("hw_id", eth0) + self.assertNotIn("hw-id", eth0) + + def test_enabled_true_when_no_disable(self): + self.set_running_config( + { + "ethernet": {"eth0": {}}, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertTrue(eth0["enabled"]) + + def test_enabled_false_when_disable_present(self): + self.set_running_config( + { + "ethernet": {"eth0": {"disable": {}}}, + }, + ) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertFalse(eth0["enabled"]) + + +class TestVyOSInterfacesIfaceCmds(unittest.TestCase): + + def test_set_description(self): + want = {"description": "WAN", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "description", "WAN"]), + cmds, + ) + + def test_set_mtu(self): + want = {"mtu": 9000, "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "mtu", "9000"]), + cmds, + ) + + def test_set_duplex(self): + want = {"duplex": "full", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "duplex", "full"]), + cmds, + ) + + def test_set_speed(self): + want = {"speed": "1000", "enabled": True} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "speed", "1000"]), + cmds, + ) + + def test_disable_interface(self): + want = {"enabled": False} + have = {"enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_enable_interface(self): + want = {"enabled": True} + have = {"enabled": False} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_idempotent_description(self): + want = {"description": "WAN", "enabled": True} + have = {"description": "WAN", "enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertEqual(cmds, []) + + def test_delete_description_when_none(self): + want = {"enabled": True} + have = {"description": "Old", "enabled": True} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "description"]), + cmds, + ) + + +class TestVyOSInterfacesDeleteIfaceConfig(unittest.TestCase): + + def test_deletes_all_l2_fields(self): + have = { + "description": "WAN", + "mtu": 1500, + "duplex": "auto", + "speed": "auto", + "enabled": True, + } + cmds = _delete_iface_config("eth0", have) + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth0", "description"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "mtu"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "duplex"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "speed"], paths) + + def test_deletes_disable_when_disabled(self): + have = {"enabled": False} + cmds = _delete_iface_config("eth0", have) + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "disable"]), + cmds, + ) + + def test_empty_have_produces_no_commands(self): + cmds = _delete_iface_config("eth0", {}) + self.assertEqual(cmds, []) + + +class TestVyOSInterfacesBuildCommands(unittest.TestCase): + + def _have_eth0(self): + return [ + { + "name": "eth0", + "description": "Management", + "mtu": 1500, + "enabled": True, + }, + ] + + def test_merged_adds_description(self): + config = [{"name": "eth0", "description": "WAN", "enabled": True}] + cmds = build_commands(config, [], "merged") + paths = [c[1] for c in cmds] + self.assertIn( + ["interfaces", "ethernet", "eth0", "description", "WAN"], + paths, + ) + + def test_merged_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_removes_l2_fields(self): + config = [{"name": "eth0", "enabled": True}] + cmds = build_commands(config, self._have_eth0(), "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth0", "description"], paths) + self.assertIn(["interfaces", "ethernet", "eth0", "mtu"], paths) + + def test_deleted_idempotent_when_no_l2(self): + have = [{"name": "eth0", "enabled": True}] + config = [{"name": "eth0", "enabled": True}] + cmds = build_commands(config, have, "deleted") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "replaced") + self.assertEqual(cmds, []) + + def test_replaced_updates_description(self): + config = [{"name": "eth0", "description": "NEW", "mtu": 1500, "enabled": True}] + cmds = build_commands(config, self._have_eth0(), "replaced") + self.assertTrue(len(cmds) > 0) + + def test_overridden_clears_interfaces_not_in_want(self): + have = [ + {"name": "eth0", "description": "Management", "enabled": True}, + {"name": "eth1", "description": "LAN", "enabled": True}, + ] + config = [{"name": "eth0", "description": "Management", "enabled": True}] + cmds = build_commands(config, have, "overridden") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "ethernet", "eth1", "description"], paths) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_l3_interfaces.py b/tests/unit/modules/test_vyos_l3_interfaces.py new file mode 100644 index 0000000..66cd1fc --- /dev/null +++ b/tests/unit/modules/test_vyos_l3_interfaces.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_l3_interfaces import ( + _addr_cmds, + _addr_list, + _normalize, + _parse_iface, + _split_addresses, + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSL3InterfacesAddrList(unittest.TestCase): + + def test_string_returns_list(self): + self.assertEqual(_addr_list("dhcp"), ["dhcp"]) + + def test_list_returned_sorted(self): + result = _addr_list(["10.0.0.2/32", "10.0.0.1/32"]) + self.assertEqual(result, ["10.0.0.1/32", "10.0.0.2/32"]) + + def test_none_returns_empty(self): + self.assertEqual(_addr_list(None), []) + + def test_empty_list_returns_empty(self): + self.assertEqual(_addr_list([]), []) + + +class TestVyOSL3InterfacesSplitAddresses(unittest.TestCase): + + def test_dhcp_goes_to_ipv4(self): + ipv4, ipv6 = _split_addresses(["dhcp"]) + self.assertIn("dhcp", ipv4) + self.assertEqual(ipv6, []) + + def test_dhcpv6_goes_to_ipv6(self): + ipv4, ipv6 = _split_addresses(["dhcpv6"]) + self.assertEqual(ipv4, []) + self.assertIn("dhcpv6", ipv6) + + def test_ipv4_cidr(self): + ipv4, ipv6 = _split_addresses(["192.0.2.1/24"]) + self.assertIn("192.0.2.1/24", ipv4) + self.assertEqual(ipv6, []) + + def test_ipv6_cidr(self): + ipv4, ipv6 = _split_addresses(["2001:db8::1/128"]) + self.assertEqual(ipv4, []) + self.assertIn("2001:db8::1/128", ipv6) + + def test_mixed(self): + ipv4, ipv6 = _split_addresses(["dhcp", "192.0.2.1/24", "2001:db8::1/128"]) + self.assertIn("dhcp", ipv4) + self.assertIn("192.0.2.1/24", ipv4) + self.assertIn("2001:db8::1/128", ipv6) + + +class TestVyOSL3InterfacesParseIface(unittest.TestCase): + + def test_parse_dhcp(self): + result = _parse_iface("eth0", {"address": "dhcp"}) + self.assertEqual(result["name"], "eth0") + self.assertEqual(result["ipv4"], [{"address": "dhcp"}]) + + def test_parse_multiple_addresses(self): + result = _parse_iface("lo", {"address": ["10.0.0.1/32", "10.0.0.2/32"]}) + addrs = [a["address"] for a in result["ipv4"]] + self.assertIn("10.0.0.1/32", addrs) + self.assertIn("10.0.0.2/32", addrs) + + def test_parse_vif(self): + result = _parse_iface( + "eth0", + { + "address": "dhcp", + "vif": {"100": {"address": "192.0.2.100/24"}}, + }, + ) + self.assertIn("vifs", result) + self.assertEqual(result["vifs"][0]["vlan_id"], 100) + self.assertEqual(result["vifs"][0]["ipv4"][0]["address"], "192.0.2.100/24") + + def test_parse_no_address(self): + result = _parse_iface("lo", {}) + self.assertNotIn("ipv4", result) + self.assertNotIn("ipv6", result) + + def test_hw_id_ignored(self): + result = _parse_iface("eth0", {"hw-id": "52:54:00:65:5a:24"}) + self.assertNotIn("hw_id", result) + self.assertNotIn("hw-id", result) + + +class TestVyOSL3InterfacesGetRunningFixture(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("l3_interfaces_running.json") + + def test_fixture_parses_eth0_dhcp(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next((e for e in result if e["name"] == "eth0"), None) + self.assertIsNotNone(eth0) + addrs = [a["address"] for a in eth0.get("ipv4", [])] + self.assertIn("dhcp", addrs) + + def test_fixture_parses_loopback_addresses(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + lo = next((e for e in result if e["name"] == "lo"), None) + self.assertIsNotNone(lo) + addrs = [a["address"] for a in lo.get("ipv4", [])] + self.assertIn("10.0.0.1/32", addrs) + self.assertIn("10.0.0.2/32", addrs) + + def test_fixture_parses_vif(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertIn("vifs", eth0) + self.assertEqual(eth0["vifs"][0]["vlan_id"], 100) + + def test_empty_interface_not_included(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + names = [e["name"] for e in result] + # loopback with no addresses should not appear + self.assertNotIn("dummy0", names) + + +class TestVyOSL3InterfacesGetRunning(VyOSModuleTestCase): + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_interface_without_address_excluded(self): + self.set_running_config( + { + "ethernet": {"eth0": {"hw-id": "52:54:00:65:5a:24"}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSL3InterfacesNormalize(unittest.TestCase): + + def test_normalize_ipv4(self): + config = [ + { + "name": "lo", + "ipv4": [{"address": "10.0.0.1/32"}], + }, + ] + result = _normalize(config) + self.assertIn("lo", result) + self.assertIn("10.0.0.1/32", result["lo"]["ipv4"]) + + def test_normalize_vif(self): + config = [ + { + "name": "eth0", + "vifs": [{"vlan_id": 100, "ipv4": [{"address": "192.0.2.100/24"}]}], + }, + ] + result = _normalize(config) + self.assertIn(100, result["eth0"]["vifs"]) + self.assertIn("192.0.2.100/24", result["eth0"]["vifs"][100]["ipv4"]) + + def test_normalize_empty(self): + result = _normalize([]) + self.assertEqual(result, {}) + + +class TestVyOSL3InterfacesAddrCmds(unittest.TestCase): + + def test_adds_new_address(self): + base = ["interfaces", "loopback", "lo"] + cmds = _addr_cmds(base, ["10.0.0.1/32"], [], "merged") + self.assertIn(("set", base + ["address", "10.0.0.1/32"]), cmds) + + def test_idempotent(self): + base = ["interfaces", "loopback", "lo"] + cmds = _addr_cmds(base, ["10.0.0.1/32"], ["10.0.0.1/32"], "merged") + self.assertEqual(cmds, []) + + def test_merged_does_not_delete_extra(self): + base = ["interfaces", "loopback", "lo"] + cmds = _addr_cmds(base, ["10.0.0.1/32"], ["10.0.0.1/32", "10.0.0.2/32"], "merged") + self.assertEqual(cmds, []) + + def test_replaced_deletes_extra(self): + base = ["interfaces", "loopback", "lo"] + cmds = _addr_cmds(base, ["10.0.0.1/32"], ["10.0.0.1/32", "10.0.0.2/32"], "replaced") + self.assertIn(("delete", base + ["address", "10.0.0.2/32"]), cmds) + + +class TestVyOSL3InterfacesBuildCommands(unittest.TestCase): + + def _have_lo(self): + return [ + { + "name": "lo", + "ipv4": [ + {"address": "10.0.0.1/32"}, + {"address": "10.0.0.2/32"}, + ], + }, + ] + + def test_merged_adds_address(self): + config = [{"name": "lo", "ipv4": [{"address": "10.0.0.3/32"}]}] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", ["interfaces", "loopback", "lo", "address", "10.0.0.3/32"]), + cmds, + ) + + def test_merged_idempotent(self): + cmds = build_commands(self._have_lo(), self._have_lo(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_removes_all(self): + cmds = build_commands([], self._have_lo(), "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "loopback", "lo", "address", "10.0.0.1/32"], paths) + self.assertIn(["interfaces", "loopback", "lo", "address", "10.0.0.2/32"], paths) + + def test_deleted_with_config_removes_interface_addresses(self): + config = [{"name": "lo"}] + cmds = build_commands(config, self._have_lo(), "deleted") + paths = [c[1] for c in cmds] + self.assertIn(["interfaces", "loopback", "lo", "address", "10.0.0.1/32"], paths) + + def test_deleted_idempotent_when_empty(self): + cmds = build_commands([], [], "deleted") + self.assertEqual(cmds, []) + + def test_replaced_removes_extra_address(self): + config = [{"name": "lo", "ipv4": [{"address": "10.0.0.1/32"}]}] + cmds = build_commands(config, self._have_lo(), "replaced") + self.assertIn( + ("delete", ["interfaces", "loopback", "lo", "address", "10.0.0.2/32"]), + cmds, + ) + + def test_overridden_removes_unlisted_interface(self): + config = [{"name": "lo", "ipv4": [{"address": "10.0.0.1/32"}]}] + have = self._have_lo() + [ + { + "name": "eth0", + "ipv4": [{"address": "192.0.2.1/24"}], + }, + ] + cmds = build_commands(config, have, "overridden") + self.assertIn( + ("delete", ["interfaces", "ethernet", "eth0", "address", "192.0.2.1/24"]), + cmds, + ) + + def test_vif_added(self): + config = [ + { + "name": "eth0", + "vifs": [{"vlan_id": 100, "ipv4": [{"address": "192.0.2.100/24"}]}], + }, + ] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", ["interfaces", "ethernet", "eth0", "vif", "100", "address", "192.0.2.100/24"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_lag_interfaces.py b/tests/unit/modules/test_vyos_lag_interfaces.py new file mode 100644 index 0000000..7c6df87 --- /dev/null +++ b/tests/unit/modules/test_vyos_lag_interfaces.py @@ -0,0 +1,293 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_lag_interfaces import ( + _bond_base, + _bond_cmds, + _normalize, + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSLagInterfacesBondBase(unittest.TestCase): + + def test_bond_base(self): + self.assertEqual( + _bond_base("bond0"), + ["interfaces", "bonding", "bond0"], + ) + + +class TestVyOSLagInterfacesGetRunningFixture(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("lag_interfaces_running.json") + + def test_fixture_parses_bond0_mode(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + bond0 = next((e for e in result if e["name"] == "bond0"), None) + self.assertIsNotNone(bond0) + self.assertEqual(bond0["mode"], "active-backup") + + def test_fixture_parses_hash_policy(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + bond0 = next(e for e in result if e["name"] == "bond0") + self.assertEqual(bond0["hash_policy"], "layer2") + + def test_fixture_parses_arp_monitor(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + bond0 = next(e for e in result if e["name"] == "bond0") + self.assertIn("arp_monitor", bond0) + self.assertEqual(bond0["arp_monitor"]["interval"], 100) + self.assertIn("192.0.2.1", bond0["arp_monitor"]["target"]) + + def test_fixture_unwraps_bonding_key(self): + # wrap in extra "bonding" key as VyOS sometimes returns + wrapped = {"bonding": self.fixture} + self.set_running_config(wrapped) + result = get_running_config(self.mock_vyos) + self.assertTrue(len(result) > 0) + self.assertEqual(result[0]["name"], "bond0") + + +class TestVyOSLagInterfacesGetRunning(VyOSModuleTestCase): + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_parses_mode(self): + self.set_running_config( + { + "bond0": {"mode": "802.3ad"}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result[0]["mode"], "802.3ad") + + def test_parses_hash_policy(self): + self.set_running_config( + { + "bond0": {"hash-policy": "layer2+3"}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result[0]["hash_policy"], "layer2+3") + + def test_parses_members(self): + self.set_running_config( + { + "bond0": {"member": {"interface": {"eth1": {}, "eth2": {}}}}, + }, + ) + result = get_running_config(self.mock_vyos) + members = [m["member"] for m in result[0]["members"]] + self.assertIn("eth1", members) + self.assertIn("eth2", members) + + def test_parses_arp_monitor_interval(self): + self.set_running_config( + { + "bond0": {"arp-monitor": {"interval": "100"}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result[0]["arp_monitor"]["interval"], 100) + + def test_parses_arp_monitor_target_dict(self): + self.set_running_config( + { + "bond0": {"arp-monitor": {"target": {"192.0.2.1": {}}}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertIn("192.0.2.1", result[0]["arp_monitor"]["target"]) + + def test_skips_type_keys(self): + self.set_running_config( + { + "bonding": {"bond0": {"mode": "802.3ad"}}, + }, + ) + result = get_running_config(self.mock_vyos) + # "bonding" key should be skipped + names = [e["name"] for e in result] + self.assertNotIn("bonding", names) + + +class TestVyOSLagInterfacesNormalize(unittest.TestCase): + + def test_normalize_basic(self): + config = [{"name": "bond0", "mode": "802.3ad", "hash_policy": "layer2"}] + result = _normalize(config) + self.assertIn("bond0", result) + self.assertEqual(result["bond0"]["mode"], "802.3ad") + self.assertEqual(result["bond0"]["hash_policy"], "layer2") + + def test_normalize_members_sorted(self): + config = [ + { + "name": "bond0", + "members": [{"member": "eth2"}, {"member": "eth1"}], + }, + ] + result = _normalize(config) + self.assertEqual(result["bond0"]["members"], ["eth1", "eth2"]) + + def test_normalize_arp_targets_sorted(self): + config = [ + { + "name": "bond0", + "arp_monitor": {"interval": 100, "target": ["192.0.2.2", "192.0.2.1"]}, + }, + ] + result = _normalize(config) + self.assertEqual(result["bond0"]["arp_targets"], ["192.0.2.1", "192.0.2.2"]) + + def test_normalize_empty(self): + result = _normalize([]) + self.assertEqual(result, {}) + + +class TestVyOSLagInterfacesBondCmds(unittest.TestCase): + + def test_set_mode(self): + want = {"mode": "802.3ad"} + cmds = _bond_cmds("bond0", want, {}) + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "mode", "802.3ad"]), + cmds, + ) + + def test_set_hash_policy(self): + want = {"hash_policy": "layer2"} + cmds = _bond_cmds("bond0", want, {}) + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "hash-policy", "layer2"]), + cmds, + ) + + def test_set_member(self): + want = {"members": ["eth1"]} + cmds = _bond_cmds("bond0", want, {}) + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "member", "interface", "eth1"]), + cmds, + ) + + def test_set_arp_interval(self): + want = {"arp_interval": 100} + cmds = _bond_cmds("bond0", want, {}) + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "arp-monitor", "interval", "100"]), + cmds, + ) + + def test_set_arp_target(self): + want = {"arp_targets": ["192.0.2.1"]} + cmds = _bond_cmds("bond0", want, {}) + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "arp-monitor", "target", "192.0.2.1"]), + cmds, + ) + + def test_idempotent_mode(self): + want = {"mode": "802.3ad"} + have = {"mode": "802.3ad"} + cmds = _bond_cmds("bond0", want, have) + self.assertEqual(cmds, []) + + def test_no_commands_when_empty_want(self): + cmds = _bond_cmds("bond0", {}, {}) + self.assertEqual(cmds, []) + + +class TestVyOSLagInterfacesBuildCommands(unittest.TestCase): + + def _have_bond0(self): + return [ + { + "name": "bond0", + "mode": "active-backup", + "hash_policy": "layer2", + "arp_monitor": {"interval": 100, "target": ["192.0.2.1"]}, + }, + ] + + def test_merged_adds_bond(self): + config = [{"name": "bond0", "mode": "802.3ad"}] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", ["interfaces", "bonding", "bond0", "mode", "802.3ad"]), + cmds, + ) + + def test_merged_idempotent(self): + cmds = build_commands(self._have_bond0(), self._have_bond0(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_removes_all(self): + cmds = build_commands([], self._have_bond0(), "deleted") + self.assertIn( + ("delete", ["interfaces", "bonding", "bond0"]), + cmds, + ) + + def test_deleted_with_name_removes_bond(self): + config = [{"name": "bond0"}] + cmds = build_commands(config, self._have_bond0(), "deleted") + self.assertIn( + ("delete", ["interfaces", "bonding", "bond0"]), + cmds, + ) + + def test_deleted_idempotent_when_empty(self): + cmds = build_commands([], [], "deleted") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + cmds = build_commands(self._have_bond0(), self._have_bond0(), "replaced") + self.assertEqual(cmds, []) + + def test_overridden_removes_unlisted_bond(self): + config = [{"name": "bond1", "mode": "802.3ad"}] + cmds = build_commands(config, self._have_bond0(), "overridden") + self.assertIn( + ("delete", ["interfaces", "bonding", "bond0"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_lldp_interfaces.py b/tests/unit/modules/test_vyos_lldp_interfaces.py new file mode 100644 index 0000000..0950991 --- /dev/null +++ b/tests/unit/modules/test_vyos_lldp_interfaces.py @@ -0,0 +1,291 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_lldp_interfaces import ( + _iface_base, + _iface_cmds, + _normalize, + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + path = os.path.join(fixtures_dir, filename) + with open(path) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSLldpInterfacesIfaceBase(unittest.TestCase): + + def test_iface_base(self): + self.assertEqual( + _iface_base("eth0"), + ["service", "lldp", "interface", "eth0"], + ) + + +class TestVyOSLldpInterfacesGetRunningFixture(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("lldp_interfaces_running.json") + + def test_fixture_parses_eth0_mode(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next((e for e in result if e["name"] == "eth0"), None) + self.assertIsNotNone(eth0) + self.assertEqual(eth0["mode"], "disable") + + def test_fixture_parses_elin(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth0 = next(e for e in result if e["name"] == "eth0") + self.assertEqual(eth0["location"]["elin"], "1234567890") + + def test_fixture_parses_coordinate_based(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + eth1 = next((e for e in result if e["name"] == "eth1"), None) + self.assertIsNotNone(eth1) + cb = eth1["location"]["coordinate_based"] + self.assertEqual(cb["latitude"], "33.524449N") + self.assertEqual(cb["longitude"], "22.267255E") + self.assertEqual(cb["altitude"], 2200) + self.assertEqual(cb["datum"], "WGS84") + + +class TestVyOSLldpInterfacesGetRunning(VyOSModuleTestCase): + + def test_empty_returns_empty_list(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_no_interface_returns_empty(self): + self.set_running_config({"snmp": "enable"}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + def test_parses_mode(self): + self.set_running_config( + { + "interface": {"eth0": {"mode": "rx-tx"}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result[0]["mode"], "rx-tx") + + def test_parses_elin(self): + self.set_running_config( + { + "interface": {"eth0": {"location": {"elin": "9876543210"}}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertEqual(result[0]["location"]["elin"], "9876543210") + + def test_parses_coordinate_based(self): + self.set_running_config( + { + "interface": { + "eth0": { + "location": { + "coordinate-based": { + "latitude": "33.524449N", + "longitude": "22.267255E", + "altitude": "2200", + "datum": "WGS84", + }, + }, + }, + }, + }, + ) + result = get_running_config(self.mock_vyos) + cb = result[0]["location"]["coordinate_based"] + self.assertEqual(cb["latitude"], "33.524449N") + self.assertEqual(cb["altitude"], 2200) + + def test_no_mode_not_in_entry(self): + self.set_running_config( + { + "interface": {"eth0": {"location": {"elin": "1234567890"}}}, + }, + ) + result = get_running_config(self.mock_vyos) + self.assertNotIn("mode", result[0]) + + +class TestVyOSLldpInterfacesNormalize(unittest.TestCase): + + def test_normalize_mode(self): + config = [{"name": "eth0", "mode": "disable"}] + result = _normalize(config) + self.assertEqual(result["eth0"]["mode"], "disable") + + def test_normalize_elin(self): + config = [{"name": "eth0", "location": {"elin": "1234567890"}}] + result = _normalize(config) + self.assertEqual(result["eth0"]["elin"], "1234567890") + + def test_normalize_coordinate_based(self): + config = [ + { + "name": "eth0", + "location": { + "coordinate_based": { + "latitude": "33.524449N", + "longitude": "22.267255E", + "altitude": 2200, + "datum": "WGS84", + }, + }, + }, + ] + result = _normalize(config) + self.assertEqual(result["eth0"]["latitude"], "33.524449N") + self.assertEqual(result["eth0"]["altitude"], 2200) + + def test_normalize_empty(self): + result = _normalize([]) + self.assertEqual(result, {}) + + +class TestVyOSLldpInterfacesIfaceCmds(unittest.TestCase): + + def test_set_mode(self): + want = {"mode": "disable"} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["service", "lldp", "interface", "eth0", "mode", "disable"]), + cmds, + ) + + def test_set_elin(self): + want = {"elin": "1234567890"} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ("set", ["service", "lldp", "interface", "eth0", "location", "elin", "1234567890"]), + cmds, + ) + + def test_set_latitude(self): + want = {"latitude": "33.524449N", "longitude": "22.267255E"} + cmds = _iface_cmds("eth0", want, {}) + self.assertIn( + ( + "set", + [ + "service", + "lldp", + "interface", + "eth0", + "location", + "coordinate-based", + "latitude", + "33.524449N", + ], + ), + cmds, + ) + + def test_idempotent_mode(self): + want = {"mode": "disable"} + have = {"mode": "disable"} + cmds = _iface_cmds("eth0", want, have) + self.assertEqual(cmds, []) + + def test_idempotent_elin(self): + want = {"elin": "1234567890"} + have = {"elin": "1234567890"} + cmds = _iface_cmds("eth0", want, have) + self.assertEqual(cmds, []) + + def test_delete_mode_when_none(self): + want = {} + have = {"mode": "disable"} + cmds = _iface_cmds("eth0", want, have) + self.assertIn( + ("delete", ["service", "lldp", "interface", "eth0", "mode"]), + cmds, + ) + + +class TestVyOSLldpInterfacesBuildCommands(unittest.TestCase): + + def _have_eth0(self): + return [ + { + "name": "eth0", + "mode": "disable", + "location": {"elin": "1234567890"}, + }, + ] + + def test_merged_adds_interface(self): + config = [{"name": "eth0", "mode": "disable"}] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", ["service", "lldp", "interface", "eth0", "mode", "disable"]), + cmds, + ) + + def test_merged_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "merged") + self.assertEqual(cmds, []) + + def test_deleted_no_config_removes_all(self): + cmds = build_commands([], self._have_eth0(), "deleted") + self.assertIn( + ("delete", ["service", "lldp", "interface", "eth0"]), + cmds, + ) + + def test_deleted_with_config_removes_named(self): + config = [{"name": "eth0"}] + cmds = build_commands(config, self._have_eth0(), "deleted") + self.assertIn( + ("delete", ["service", "lldp", "interface", "eth0"]), + cmds, + ) + + def test_deleted_idempotent_when_empty(self): + cmds = build_commands([], [], "deleted") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + cmds = build_commands(self._have_eth0(), self._have_eth0(), "replaced") + self.assertEqual(cmds, []) + + def test_overridden_removes_unlisted(self): + config = [{"name": "eth1", "mode": "rx-tx"}] + cmds = build_commands(config, self._have_eth0(), "overridden") + self.assertIn( + ("delete", ["service", "lldp", "interface", "eth0"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_logging_global.py b/tests/unit/modules/test_vyos_logging_global.py index 8593f1f..7d91182 100644 --- a/tests/unit/modules/test_vyos_logging_global.py +++ b/tests/unit/modules/test_vyos_logging_global.py @@ -98,7 +98,7 @@ class TestVyOSLoggingGlobalNormalize(unittest.TestCase): def test_normalize_running_host_port_not_cast(self): """Port is NOT cast to int — stored as-is from API response.""" raw = { - "host": { + "remote": { "172.16.0.1": { "port": "514", "facility": {}, @@ -112,7 +112,7 @@ class TestVyOSLoggingGlobalNormalize(unittest.TestCase): def test_normalize_running_global_archive_key(self): """Archive stored under 'archive' key — no file_num remapping.""" raw = { - "global": { + "local": { "archive": {"file": "2", "size": "111"}, "marker": {"interval": "111"}, "preserve-fqdn": {}, @@ -133,7 +133,7 @@ class TestVyOSLoggingGlobalNormalize(unittest.TestCase): def test_normalize_running_host_facilities(self): raw = { - "host": { + "remote": { "172.16.0.1": { "facility": { "local7": {"level": "all"}, @@ -196,14 +196,14 @@ class TestVyOSLoggingGlobalBuildCommands(unittest.TestCase): cmds = build_commands(want, self._empty_have(), "merged") paths = [c[1] for c in cmds] # diff_map only adds the host key, not per-facility details - self.assertIn(["system", "syslog", "host", "172.16.0.1"], paths) + self.assertIn(["system", "syslog", "remote", "172.16.0.1"], paths) def test_replaced_removes_extra_host(self): want = self._empty_have() have = self._empty_have() have["hosts"]["172.16.0.1"] = {"port": None, "facilities": {}} cmds = build_commands(want, have, "replaced") - self.assertIn(("delete", ["system", "syslog", "host", "172.16.0.1"]), cmds) + self.assertIn(("delete", ["system", "syslog", "remote", "172.16.0.1"]), cmds) def test_deleted_removes_per_field(self): """deleted state removes per-facility entries, not single subtree.""" diff --git a/tests/unit/modules/test_vyos_ospf_interfaces.py b/tests/unit/modules/test_vyos_ospf_interfaces.py new file mode 100644 index 0000000..4d6be0d --- /dev/null +++ b/tests/unit/modules/test_vyos_ospf_interfaces.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_ospf_interfaces import ( + _parse_ipv4_iface, + _parse_ipv6_iface, + build_commands, + get_running_config, +) + + +_BASE4 = ["protocols", "ospf", "interface"] +_BASE6 = ["protocols", "ospfv3", "interface"] + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + with open(os.path.join(fixtures_dir, filename)) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.ipv4_fixture = load_fixture("ospf_interfaces_ipv4.json") + self.ipv6_fixture = load_fixture("ospf_interfaces_ipv6.json") + self.mock_vyos.get_config = MagicMock( + side_effect=lambda path: (self.ipv4_fixture if path == _BASE4 else self.ipv6_fixture), + ) + + +class TestVyOSOspfInterfacesParse(VyOSModuleTestCase): + + def test_parse_ipv4_scalars(self): + raw = {"cost": "100", "transmit-delay": "50", "priority": "26"} + result = _parse_ipv4_iface(raw) + self.assertEqual(result["afi"], "ipv4") + self.assertEqual(result["cost"], 100) + self.assertEqual(result["transmit_delay"], 50) + self.assertEqual(result["priority"], 26) + + def test_parse_ipv4_md5_auth(self): + raw = {"authentication": {"md5": {"key-id": {"10": {"md5-key": "secret"}}}}} + result = _parse_ipv4_iface(raw) + self.assertEqual(result["authentication"]["md5_key"]["key_id"], 10) + self.assertEqual(result["authentication"]["md5_key"]["key"], "secret") + + def test_parse_ipv6_scalars(self): + raw = {"dead-interval": "39", "passive": {}} + result = _parse_ipv6_iface(raw) + self.assertEqual(result["afi"], "ipv6") + self.assertEqual(result["dead_interval"], 39) + self.assertTrue(result["passive"]) + + def test_get_running_config(self): + result = get_running_config(self.mock_vyos) + names = [e["name"] for e in result] + self.assertIn("eth1", names) + self.assertIn("eth2", names) + eth1 = next(e for e in result if e["name"] == "eth1") + afis = {af["afi"] for af in eth1["address_family"]} + self.assertIn("ipv4", afis) + self.assertIn("ipv6", afis) + + def test_get_running_config_empty(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSOspfInterfacesBuildCommands(unittest.TestCase): + + def test_deleted_all(self): + have = [ + { + "name": "eth1", + "address_family": [{"afi": "ipv4", "cost": 100}, {"afi": "ipv6", "passive": True}], + }, + ] + cmds = build_commands([], have, "deleted") + self.assertIn(("delete", _BASE4 + ["eth1"]), cmds) + self.assertIn(("delete", _BASE6 + ["eth1"]), cmds) + + def test_deleted_specific(self): + have = [ + {"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}, + {"name": "eth2", "address_family": [{"afi": "ipv4", "cost": 200}]}, + ] + cmds = build_commands([{"name": "eth1"}], have, "deleted") + self.assertIn(("delete", _BASE4 + ["eth1"]), cmds) + paths = [c[1] for c in cmds] + self.assertNotIn(_BASE4 + ["eth2"], paths) + + def test_merged_adds_cost(self): + config = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}] + cmds = build_commands(config, [], "merged") + self.assertIn(("set", _BASE4 + ["eth1", "cost", "100"]), cmds) + + def test_merged_idempotent(self): + have = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}] + config = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}] + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_ipv6_passive(self): + config = [{"name": "eth1", "address_family": [{"afi": "ipv6", "passive": True}]}] + cmds = build_commands(config, [], "merged") + self.assertIn(("set", _BASE6 + ["eth1", "passive"]), cmds) + + def test_overridden_removes_extra_interface(self): + have = [ + {"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}, + {"name": "eth2", "address_family": [{"afi": "ipv4", "cost": 200}]}, + ] + config = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 100}]}] + cmds = build_commands(config, have, "overridden") + self.assertIn(("delete", _BASE4 + ["eth2"]), cmds) + + def test_replaced_idempotent(self): + have = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 200}]}] + config = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 200}]}] + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + have = [ + { + "name": "eth1", + "address_family": [{"afi": "ipv4", "cost": 100, "transmit_delay": 50}], + }, + ] + config = [{"name": "eth1", "address_family": [{"afi": "ipv4", "cost": 200}]}] + cmds = build_commands(config, have, "replaced") + self.assertIn(("delete", _BASE4 + ["eth1"]), cmds) + self.assertIn(("set", _BASE4 + ["eth1", "cost", "200"]), cmds) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_ospfv2.py b/tests/unit/modules/test_vyos_ospfv2.py new file mode 100644 index 0000000..c05dd77 --- /dev/null +++ b/tests/unit/modules/test_vyos_ospfv2.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_ospfv2 import ( + _parse_areas, + _parse_default_information, + _parse_neighbor, + _parse_parameters, + _parse_redistribute, + build_commands, + get_running_config, +) + + +_BASE = ["protocols", "ospf"] + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + with open(os.path.join(fixtures_dir, filename)) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.fixture = load_fixture("ospfv2_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestVyOSOspfv2Parse(VyOSModuleTestCase): + + def test_parse_parameters(self): + result = _parse_parameters(self.fixture["parameters"]) + self.assertEqual(result["router_id"], "192.0.1.1") + self.assertEqual(result["abr_type"], "cisco") + self.assertTrue(result["opaque_lsa"]) + self.assertTrue(result["rfc1583_compatibility"]) + + def test_parse_redistribute(self): + result = _parse_redistribute(self.fixture["redistribute"]) + route_types = [r["route_type"] for r in result] + self.assertIn("bgp", route_types) + self.assertIn("connected", route_types) + bgp = next(r for r in result if r["route_type"] == "bgp") + self.assertEqual(bgp["metric"], 10) + self.assertEqual(bgp["metric_type"], 2) + + def test_parse_neighbor(self): + result = _parse_neighbor(self.fixture["neighbor"]) + self.assertEqual(len(result), 1) + nb = result[0] + self.assertEqual(nb["neighbor_id"], "192.0.11.12") + self.assertEqual(nb["priority"], 2) + self.assertEqual(nb["poll_interval"], 10) + + def test_parse_default_information(self): + result = _parse_default_information(self.fixture["default-information"]) + orig = result["originate"] + self.assertTrue(orig["always"]) + self.assertEqual(orig["metric"], 10) + self.assertEqual(orig["metric_type"], 2) + self.assertEqual(orig["route_map"], "ingress") + + def test_parse_areas(self): + result = _parse_areas(self.fixture["area"]) + self.assertEqual(len(result), 3) + area2 = next(a for a in result if a["area_id"] == "2") + self.assertTrue(area2["area_type"]["normal"]) + self.assertEqual(area2["network"][0]["address"], "192.0.2.0/24") + + area3 = next(a for a in result if a["area_id"] == "3") + self.assertTrue(area3["area_type"]["nssa"]["set"]) + + area4 = next(a for a in result if a["area_id"] == "4") + self.assertEqual(area4["area_type"]["stub"]["default_cost"], 20) + self.assertEqual(len(area4["range"]), 2) + r = next(r for r in area4["range"] if r["address"] == "192.0.3.0/24") + self.assertEqual(r["cost"], 10) + + def test_get_running_config(self): + result = get_running_config(self.mock_vyos) + self.assertEqual(result["parameters"]["router_id"], "192.0.1.1") + self.assertIn("eth1", result["passive_interface"]) + self.assertIn("eth2", result["passive_interface"]) + self.assertEqual(result["auto_cost"]["reference_bandwidth"], 2) + self.assertEqual(result["log_adjacency_changes"], "detail") + + def test_get_running_config_empty(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSOspfv2BuildCommands(unittest.TestCase): + + def test_deleted_with_have(self): + have = {"parameters": {"router_id": "192.0.1.1"}} + cmds = build_commands({}, have, "deleted") + self.assertEqual(cmds, [("delete", _BASE)]) + + def test_deleted_without_have(self): + cmds = build_commands({}, {}, "deleted") + self.assertEqual(cmds, []) + + def test_merged_parameters(self): + config = {"parameters": {"router_id": "192.0.1.1"}} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["parameters", "router-id", "192.0.1.1"]), cmds) + + def test_merged_redistribute(self): + config = {"redistribute": [{"route_type": "bgp", "metric": 10}]} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["redistribute", "bgp"]), cmds) + self.assertIn(("set", _BASE + ["redistribute", "bgp", "metric", "10"]), cmds) + + def test_merged_passive_interface(self): + config = {"passive_interface": ["eth1"]} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["interface", "eth1", "passive"]), cmds) + + def test_merged_area_normal(self): + config = {"areas": [{"area_id": "2", "area_type": {"normal": True}}]} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["area", "2", "area-type", "normal"]), cmds) + + def test_merged_area_stub_with_cost(self): + config = {"areas": [{"area_id": "4", "area_type": {"stub": {"default_cost": 20}}}]} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["area", "4", "area-type", "stub", "default-cost", "20"]), + cmds, + ) + + def test_merged_idempotent(self): + have = {"parameters": {"router_id": "192.0.1.1"}} + config = {"parameters": {"router_id": "192.0.1.1"}} + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + have = {"parameters": {"router_id": "192.0.1.1"}} + config = {"parameters": {"router_id": "192.0.1.1"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + have = {"parameters": {"router_id": "192.0.1.1"}} + config = {"parameters": {"router_id": "192.0.1.2"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds[0], ("delete", _BASE)) + self.assertIn( + ("set", _BASE + ["parameters", "router-id", "192.0.1.2"]), + cmds, + ) + + def test_merged_neighbor(self): + config = {"neighbor": [{"neighbor_id": "192.0.11.12", "priority": 2}]} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["neighbor", "192.0.11.12"]), cmds) + self.assertIn(("set", _BASE + ["neighbor", "192.0.11.12", "priority", "2"]), cmds) + + def test_merged_default_information(self): + config = {"default_information": {"originate": {"always": True, "metric": 10}}} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["default-information", "originate", "always"]), cmds) + self.assertIn(("set", _BASE + ["default-information", "originate", "metric", "10"]), cmds) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_ospfv3.py b/tests/unit/modules/test_vyos_ospfv3.py new file mode 100644 index 0000000..a84041b --- /dev/null +++ b/tests/unit/modules/test_vyos_ospfv3.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +import json +import os +import unittest + +from unittest.mock import MagicMock + +from ansible_collections.vyos.rest.plugins.modules.vyos_ospfv3 import ( + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + with open(os.path.join(fixtures_dir, filename)) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSOspfv3Parse(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("ospfv3_running.json") + + def test_parses_parameters(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertEqual(result["parameters"]["router_id"], "192.0.2.10") + + def test_parses_redistribute(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + route_types = [r["route_type"] for r in result["redistribute"]] + self.assertIn("bgp", route_types) + self.assertIn("connected", route_types) + connected = next(r for r in result["redistribute"] if r["route_type"] == "connected") + self.assertEqual(connected["route_map"], "RM1") + + def test_parses_areas(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertEqual(len(result["areas"]), 2) + area2 = next(a for a in result["areas"] if a["area_id"] == "2") + self.assertEqual(area2["export_list"], "export1") + self.assertEqual(area2["import_list"], "import1") + self.assertEqual(len(area2["range"]), 2) + not_adv = next(r for r in area2["range"] if r["address"] == "2001:db20::/32") + self.assertTrue(not_adv["not_advertise"]) + + def test_empty_config_returns_empty(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSOspfv3BuildCommands(unittest.TestCase): + + def test_deleted_with_have(self): + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands({}, have, "deleted") + self.assertEqual(cmds, [("delete", ["protocols", "ospfv3"])]) + + def test_deleted_without_have(self): + cmds = build_commands({}, {}, "deleted") + self.assertEqual(cmds, []) + + def test_merged_parameters(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "parameters", "router-id", "192.0.2.10"]), + cmds, + ) + + def test_merged_redistribute(self): + config = {"redistribute": [{"route_type": "bgp"}]} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "redistribute", "bgp"]), + cmds, + ) + + def test_merged_idempotent(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_area_range(self): + config = { + "areas": [{"area_id": "2", "range": [{"address": "2001:db10::/32"}]}], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "area", "2", "range", "2001:db10::/32"]), + cmds, + ) + + def test_replaced_idempotent(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + config = {"parameters": {"router_id": "192.0.2.11"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds[0], ("delete", ["protocols", "ospfv3"])) + self.assertIn( + ("set", ["protocols", "ospfv3", "parameters", "router-id", "192.0.2.11"]), + cmds, + ) + + def test_merged_area_export_list(self): + config = {"areas": [{"area_id": "2", "export_list": "export1"}]} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "area", "2", "export-list", "export1"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() |
