diff options
113 files changed, 11028 insertions, 0 deletions
@@ -77,7 +77,13 @@ Name | Description 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_bgp_address_family](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_bgp_address_family_module.rst)|Manage BGP address-family configuration on VyOS devices using REST API +[vyos.rest.vyos_bgp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_bgp_global_module.rst)|Manage BGP global configuration on VyOS devices using 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_facts](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_facts_module.rst)|Get facts about VyOS devices using REST API +[vyos.rest.vyos_firewall_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_firewall_global_module.rst)|Manage global firewall configuration on VyOS devices using REST API +[vyos.rest.vyos_firewall_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_firewall_interfaces_module.rst)|Manage firewall hook filters on VyOS devices using REST API +[vyos.rest.vyos_firewall_rules](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_firewall_rules_module.rst)|Manage firewall rule sets on VyOS devices using 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. @@ -94,6 +100,7 @@ erfaces_module.rst)|Manage LLDP interface configuration on VyOS devices via REST [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. +[vyos.rest.vyos_user](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_user_module.rst)|Manage user accounts on VyOS devices using REST API <!--end collection content--> diff --git a/docs/vyos.rest.vyos_bgp_address_family_module.rst b/docs/vyos.rest.vyos_bgp_address_family_module.rst new file mode 100644 index 0000000..d9d45d7 --- /dev/null +++ b/docs/vyos.rest.vyos_bgp_address_family_module.rst @@ -0,0 +1,1024 @@ +.. _vyos.rest.vyos_bgp_address_family_module: + + +********************************* +vyos.rest.vyos_bgp_address_family +********************************* + +**Manage BGP address-family configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages BGP address-family configuration on VyOS devices via the REST API. +- Covers global address-family (networks, redistribution) and per-neighbor address-family settings. +- BGP must be configured first using :ref:`vyos.rest.vyos_bgp_global <vyos.rest.vyos_bgp_global_module>`. +- 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">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>BGP address-family configuration.</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>Global BGP address-family settings.</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>networks</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 to advertise.</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>backdoor</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>Network backdoor.</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>prefix</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 prefix.</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 to apply.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <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 other protocols.</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 redistributed routes.</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>protocol</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>connected</li> + <li>kernel</li> + <li>ospf</li> + <li>ospfv3</li> + <li>rip</li> + <li>ripng</li> + <li>static</li> + </ul> + </td> + <td> + <div>Protocol to redistribute.</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 to apply.</div> + </td> + </tr> + + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>as_number</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>BGP autonomous system number (required for context).</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>neighbors</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>Per-neighbor address-family settings.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <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>Address-family settings for this neighbor.</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>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 class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>allowas_in</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>Accept as-path with my AS present.</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>attribute_unchanged</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>BGP attributes to leave unchanged.</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>as_path</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>Leave as-path unchanged.</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>med</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>Leave MED unchanged.</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>next_hop</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>Leave next-hop unchanged.</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>capability</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>Advertise capability to the peer.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>orf</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>receive</li> + <li>send</li> + </ul> + </td> + <td> + <div>ORF capability.</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>default_originate</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>Send default route to neighbor.</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>distribute_list</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>Filter updates using access-list.</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>export</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>Access-list to filter outbound updates.</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>import</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>Access-list to filter inbound updates.</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>maximum_prefix</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Maximum number of prefixes to accept.</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>nexthop_self</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>Set next-hop to self.</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>prefix_list</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>Filter updates using prefix-list.</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>export</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>Prefix-list to filter outbound updates.</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>import</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>Prefix-list to filter inbound updates.</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">dictionary</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 class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>export</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 for outbound updates.</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>import</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 for inbound updates.</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_reflector_client</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>Configure as route reflector client.</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_server_client</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>Configure as route server client.</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>soft_reconfiguration</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 soft reconfiguration inbound.</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>unsuppress_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 selectively unsuppress suppressed routes.</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>weight</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 weight for routes from this neighbor.</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_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>Neighbor IP address.</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 of the BGP address-family configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces the entire BGP address-family configuration.</div> + <div><code>deleted</code> removes BGP address-family 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``. + - BGP must be configured first using :ref:`vyos.rest.vyos_bgp_global <vyos.rest.vyos_bgp_global_module>`. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + redistribute: + - protocol: connected + metric: 10 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + nexthop_self: true + - afi: ipv6 + soft_reconfiguration: true + state: merged + + - name: Delete all BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + state: deleted + + - name: Gather BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + 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>BGP address-family 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>BGP address-family 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 BGP address-family 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_bgp_global_module.rst b/docs/vyos.rest.vyos_bgp_global_module.rst new file mode 100644 index 0000000..01bcefe --- /dev/null +++ b/docs/vyos.rest.vyos_bgp_global_module.rst @@ -0,0 +1,876 @@ +.. _vyos.rest.vyos_bgp_global_module: + + +************************* +vyos.rest.vyos_bgp_global +************************* + +**Manage BGP global configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages BGP global configuration on VyOS devices via the REST API. +- Covers system AS, parameters, neighbors, and peer-groups. +- For per-neighbor address-family configuration use :ref:`vyos.rest.vyos_bgp_address_family <vyos.rest.vyos_bgp_address_family_module>`. +- 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>BGP global configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>as_number</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>BGP autonomous system number.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>neighbors</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>BGP neighbors.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <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>Neighbor description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>disable_connected_check</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 connected route check.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ebgp_multihop</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>EBGP multihop TTL.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>local_as</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Local AS number.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>neighbor_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>Neighbor IP address.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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>MD5 password for neighbor.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>peer_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Peer group name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>remote_as</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 AS number.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>shutdown</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>Shutdown neighbor.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>timers</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>Neighbor timers.</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>holdtime</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>Hold time in seconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>keepalive</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>Keepalive interval in seconds.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>update_source</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>Source interface/IP for updates.</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>BGP 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>bestpath</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>BGP bestpath parameters.</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>as_path</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>confed</li> + <li>ignore</li> + <li>multipath-relax</li> + </ul> + </td> + <td> + <div>AS-path attribute comparison.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>confederation</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>AS confederation parameters.</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>identifier</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>Confederation AS identifier.</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>peers</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=integer</span> + </div> + </td> + <td> + </td> + <td> + <div>Peer ASs in confederation.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>graceful_restart</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 graceful restart.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>log_neighbor_changes</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>Log neighbor up/down changes.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>no_ipv4_unicast</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 IPv4 unicast default.</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>BGP router ID.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>peer_groups</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>BGP peer groups.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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>Peer group description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>ebgp_multihop</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>EBGP multihop TTL.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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>MD5 password.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>peer_group</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>Peer group name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>remote_as</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>Peer group AS number.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>timers</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Peer group timers.</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>holdtime</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>Hold time in seconds.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>keepalive</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>Keepalive interval in seconds.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>update_source</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>Source interface/IP for updates.</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 BGP global configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces the entire BGP configuration.</div> + <div><code>deleted</code> removes BGP 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``. + - BGP system-as must be defined before any other BGP configuration. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge BGP global configuration + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + description: peer1 + timers: + holdtime: 30 + keepalive: 10 + peer_groups: + - peer_group: PG1 + remote_as: 65002 + state: merged + + - name: Delete BGP configuration + vyos.rest.vyos_bgp_global: + state: deleted + + - name: Gather BGP global configuration + vyos.rest.vyos_bgp_global: + 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>BGP 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>BGP 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 BGP 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_facts_module.rst b/docs/vyos.rest.vyos_facts_module.rst new file mode 100644 index 0000000..d5c6a89 --- /dev/null +++ b/docs/vyos.rest.vyos_facts_module.rst @@ -0,0 +1,296 @@ +.. _vyos.rest.vyos_facts_module: + + +******************** +vyos.rest.vyos_facts +******************** + +**Get facts about VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Collects facts from VyOS devices via the REST API. +- Returns structured facts under the ``ansible_facts`` key. +- Uses REST API (``connection=httpapi``) instead of CLI. + + + + +Parameters +---------- + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="1">Parameter</th> + <th>Choices/<font color="blue">Defaults</font></th> + <th width="100%">Comments</th> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>gather_network_resources</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>When supplied, this argument will restrict the facts collected to a given subset. Possible values include the resource module names.</div> + <div>This argument is not currently used.</div> + </td> + </tr> + <tr> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>gather_subset</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> + <b>Default:</b><br/><div style="color: blue">["default"]</div> + </td> + <td> + <div>When supplied, this argument will restrict the facts collected to a given subset. Possible values for this argument include <code>all</code>, <code>default</code>, <code>config</code>, <code>interfaces</code>, <code>hostname</code>, <code>users</code>, <code>bgp</code>, <code>ospf</code>, <code>ntp</code>, <code>snmp</code> and <code>logging</code>.</div> + <div>Specify a list of values to include a larger subset. Use the exclamation mark (<code>!</code>) before a value to exclude it. Values <code>all</code> and <code>default</code> cannot be combined with each other or with negation.</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``. + - Only configuration facts are available via the REST API. Operational state (interface counters, BGP neighbors) is not supported. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Gather all facts + vyos.rest.vyos_facts: + gather_subset: all + + - name: Gather default facts + vyos.rest.vyos_facts: + + - name: Gather interface and hostname facts only + vyos.rest.vyos_facts: + gather_subset: + - interfaces + - hostname + + - name: Gather all except config + vyos.rest.vyos_facts: + gather_subset: + - all + - '!config' + + +Returned Facts +-------------- +Facts returned by this module are added/updated in the ``hostvars`` host facts and can be referenced by name just like any other host fact. They do not need to be registered in order to use them. + +.. raw:: html + + <table border=0 cellpadding=0 class="documentation-table"> + <tr> + <th colspan="1">Fact</th> + <th>Returned</th> + <th width="100%">Description</th> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_bgp</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>BGP configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_config</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>Full device configuration as structured data. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_hostname</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td></td> + <td> + <div>Device hostname. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_interfaces</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>Interface configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_logging</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>Logging configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_ntp</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>NTP configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_ospf</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>OSPFv2 configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_ospfv3</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>OSPFv3 configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_snmp</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">dictionary</span> + </div> + </td> + <td></td> + <td> + <div>SNMP configuration. + </div> + <br/> + </td> + </tr> + <tr> + <td colspan="1" colspan="1"> + <div class="ansibleOptionAnchor" id="return-"></div> + <b>vyos_users</b> + <a class="ansibleOptionLink" href="#return-" title="Permalink to this fact"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + </div> + </td> + <td></td> + <td> + <div>User accounts (without passwords). + </div> + <br/> + </td> + </tr> + </table> + <br/><br/> + + + +Status +------ + + +Authors +~~~~~~~ + +- VyOS Community (@vyos) diff --git a/docs/vyos.rest.vyos_firewall_global_module.rst b/docs/vyos.rest.vyos_firewall_global_module.rst new file mode 100644 index 0000000..d90c96a --- /dev/null +++ b/docs/vyos.rest.vyos_firewall_global_module.rst @@ -0,0 +1,642 @@ +.. _vyos.rest.vyos_firewall_global_module: + + +****************************** +vyos.rest.vyos_firewall_global +****************************** + +**Manage global firewall configuration on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages global firewall group configuration on VyOS devices via the REST API. +- Covers address-groups, network-groups, port-groups, interface-groups, and IPv6 network-groups. +- 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>Global firewall configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>group</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>Firewall groups.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>address_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv4 address groups.</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">list</span> + / <span style="color: purple">elements=string</span> + </div> + </td> + <td> + </td> + <td> + <div>IP addresses or ranges in the group.</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>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>Group description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Group name.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>interface_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Interface groups.</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>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>Group description.</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>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>Interfaces in the group.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Group name.</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_network_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 network groups.</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>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>Group description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Group name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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=string</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv6 network prefixes in the group.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>network_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>IPv4 network groups.</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>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>Group description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Group name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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=string</span> + </div> + </td> + <td> + </td> + <td> + <div>Network prefixes in the group.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port_group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">list</span> + / <span style="color: purple">elements=dictionary</span> + </div> + </td> + <td> + </td> + <td> + <div>Port groups.</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>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>Group description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Group name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>port</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>Ports or port ranges in the group.</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 firewall global configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces the entire firewall global configuration.</div> + <div><code>deleted</code> removes firewall global 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 firewall global configuration + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + description: Web servers + address: + - 192.168.1.10 + - 192.168.1.11 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + port_group: + - name: WEB-PORTS + port: + - "80" + - "443" + interface_group: + - name: LAN-IFACES + interface: + - eth1 + - eth2 + ipv6_network_group: + - name: IPV6-LAN + network: + - "2001:db8::/32" + state: merged + + - name: Delete all firewall global configuration + vyos.rest.vyos_firewall_global: + state: deleted + + - name: Gather firewall global configuration + vyos.rest.vyos_firewall_global: + 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>Firewall global 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>Firewall global 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 firewall global 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_firewall_interfaces_module.rst b/docs/vyos.rest.vyos_firewall_interfaces_module.rst new file mode 100644 index 0000000..56effe1 --- /dev/null +++ b/docs/vyos.rest.vyos_firewall_interfaces_module.rst @@ -0,0 +1,632 @@ +.. _vyos.rest.vyos_firewall_interfaces_module: + + +********************************** +vyos.rest.vyos_firewall_interfaces +********************************** + +**Manage firewall hook filters on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages firewall hook filter configuration on VyOS devices via the REST API. +- In VyOS 1.5+, firewall hook filters (input/output/forward) replace the per-interface firewall assignments used in VyOS 1.4. +- Hook filters apply globally to all traffic traversing that hook point. +- 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>Firewall hook filter configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <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.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hooks</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>Hook filter configurations for this 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>default_action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>accept</li> + <li>drop</li> + <li>reject</li> + </ul> + </td> + <td> + <div>Default action when no rule matches.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <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>Filter description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>hook</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>input</li> + <li>output</li> + <li>forward</li> + </ul> + </td> + <td> + <div>Netfilter hook point.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rules</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>Rules in this hook filter.</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>action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>accept</li> + <li>drop</li> + <li>reject</li> + <li>return</li> + <li>queue</li> + <li>continue</li> + </ul> + </td> + <td> + <div>Rule action.</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>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>Rule description.</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>destination</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>Destination match criteria.</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>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>Destination IP address or prefix.</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>port</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>Destination port or 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>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this rule.</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>log</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 logging.</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>number</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>Rule number.</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>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to match.</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>source</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>Source match criteria.</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>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>Source IP address or prefix.</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>port</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>Source port or 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>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>established</li> + <li>invalid</li> + <li>new</li> + <li>related</li> + </ul> + </td> + <td> + <div>Connection state to match.</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 firewall hook filter configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces hook filter config for named hooks in config.</div> + <div><code>overridden</code> replaces all firewall hook filter config.</div> + <div><code>deleted</code> removes firewall hook filter 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``. + - In VyOS 1.5+, hook filters apply globally rather than per-interface. Use named rule sets (:ref:`vyos.rest.vyos_firewall_rules <vyos.rest.vyos_firewall_rules_module>`) for more granular per-traffic control. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge firewall hook filter configuration + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + - number: 20 + action: drop + state: invalid + - hook: forward + default_action: accept + - hook: output + default_action: accept + - afi: ipv6 + hooks: + - hook: input + default_action: accept + state: merged + + - name: Delete all firewall hook filter configuration + vyos.rest.vyos_firewall_interfaces: + state: deleted + + - name: Gather firewall hook filter configuration + vyos.rest.vyos_firewall_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>Firewall hook filter 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>Firewall hook filter 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 firewall hook filter 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_firewall_rules_module.rst b/docs/vyos.rest.vyos_firewall_rules_module.rst new file mode 100644 index 0000000..b0e32c8 --- /dev/null +++ b/docs/vyos.rest.vyos_firewall_rules_module.rst @@ -0,0 +1,725 @@ +.. _vyos.rest.vyos_firewall_rules_module: + + +***************************** +vyos.rest.vyos_firewall_rules +***************************** + +**Manage firewall rule sets on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages named firewall rule sets on VyOS devices via the REST API. +- Supports both IPv4 (``ipv4``) and IPv6 (``ipv6``) rule sets. +- Uses REST API (``connection=httpapi``) instead of CLI. +- In VyOS 1.5+, firewall uses named rule sets under ``firewall.ipv4.name`` and ``firewall.ipv6.name``. + + + + +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>Firewall rule set configuration.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <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.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="4"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rule_sets</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>Named rule sets for this 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>default_action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>accept</li> + <li>drop</li> + <li>reject</li> + </ul> + </td> + <td> + <div>Default action when no rule matches.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <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>Rule set description.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <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>Rule set name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>rules</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>Firewall rules in this rule set.</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>action</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>accept</li> + <li>drop</li> + <li>reject</li> + <li>return</li> + <li>queue</li> + <li>continue</li> + </ul> + </td> + <td> + <div>Rule action.</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>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>Rule description.</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>destination</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>Destination match criteria.</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>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>Destination IP address or prefix.</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>group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Destination group name.</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>port</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>Destination port or 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>disable</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">boolean</span> + </div> + </td> + <td> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li>no</li> + <li>yes</li> + </ul> + </td> + <td> + <div>Disable this rule.</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>icmp</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>ICMP type/code to match.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>code</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>ICMP code.</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>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>ICMP 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>log</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 logging for this rule.</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>number</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>Rule number.</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>protocol</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Protocol to match.</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>source</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>Source match criteria.</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>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>Source IP address or prefix.</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>group</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + </div> + </td> + <td> + </td> + <td> + <div>Source group name.</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>port</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>Source port or 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>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>established</li> + <li>invalid</li> + <li>new</li> + <li>related</li> + </ul> + </td> + <td> + <div>Connection state to match.</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 firewall rules configuration.</div> + <div><code>merged</code> adds or updates without removing existing config.</div> + <div><code>replaced</code> replaces rule sets for named rule sets in config.</div> + <div><code>overridden</code> replaces all firewall rule sets.</div> + <div><code>deleted</code> removes firewall rule sets.</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``. + - Rule sets are identified by AFI and name. Deleting a rule set removes all its rules. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Merge firewall rules + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + source: + address: 192.168.1.0/24 + destination: + port: "80" + - number: 20 + action: drop + state: invalid + - afi: ipv6 + rule_sets: + - name: RULE-SET6 + default_action: accept + rules: + - number: 10 + action: accept + state: merged + + - name: Delete all firewall rules + vyos.rest.vyos_firewall_rules: + state: deleted + + - name: Gather firewall rules + vyos.rest.vyos_firewall_rules: + 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>Firewall rules 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>Firewall rules 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 firewall rules 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_user_module.rst b/docs/vyos.rest.vyos_user_module.rst new file mode 100644 index 0000000..1ad08f4 --- /dev/null +++ b/docs/vyos.rest.vyos_user_module.rst @@ -0,0 +1,393 @@ +.. _vyos.rest.vyos_user_module: + + +******************* +vyos.rest.vyos_user +******************* + +**Manage user accounts on VyOS devices using REST API** + + +Version added: 1.0.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- Manages local user accounts on VyOS devices via the REST API. +- Uses REST API (``connection=httpapi``) instead of CLI. +- Passwords are write-only. Once set, they cannot be read back in plaintext. +- Use ``update_password=on_create`` to avoid resetting passwords on every run. + + + + +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>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>present</b> ←</div></li> + <li>absent</li> + <li>gathered</li> + </ul> + </td> + <td> + <div><code>present</code> ensures users exist with the specified configuration.</div> + <div><code>absent</code> removes specified users.</div> + <div><code>gathered</code> returns current user configuration as structured data.</div> + </td> + </tr> + <tr> + <td colspan="3"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>users</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 user definitions.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>full_name</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>Full name of the user.</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>Username.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>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. Write-only — hashed on device immediately.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>public_keys</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>SSH public keys for the user.</div> + </td> + </tr> + <tr> + <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> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Base64-encoded public key.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>name</b> + <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a> + <div style="font-size: small"> + <span style="color: purple">string</span> + / <span style="color: red">required</span> + </div> + </td> + <td> + </td> + <td> + <div>Key identifier/name.</div> + </td> + </tr> + <tr> + <td class="elbow-placeholder"></td> + <td class="elbow-placeholder"></td> + <td colspan="1"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>type</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>ssh-dss</li> + <li>ssh-rsa</li> + <li>ecdsa-sha2-nistp256</li> + <li>ecdsa-sha2-nistp384</li> + <li>ecdsa-sha2-nistp521</li> + <li>ssh-ed25519</li> + </ul> + </td> + <td> + <div>Key type.</div> + </td> + </tr> + + <tr> + <td class="elbow-placeholder"></td> + <td colspan="2"> + <div class="ansibleOptionAnchor" id="parameter-"></div> + <b>update_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> + <ul style="margin: 0; padding: 0"><b>Choices:</b> + <li><div style="color: blue"><b>always</b> ←</div></li> + <li>on_create</li> + </ul> + </td> + <td> + <div>Control when password is updated.</div> + <div><code>always</code> updates the password on every run (default).</div> + <div><code>on_create</code> only sets the password when the user is first created.</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``. + - The ``vyos`` user cannot be deleted as it is required for API access. + - Passwords are hashed immediately by VyOS and cannot be read back. + + + +Examples +-------- + +.. code-block:: yaml + + - name: Create user + vyos.rest.vyos_user: + users: + - name: alice + full_name: Alice Smith + password: securepassword + update_password: on_create + state: present + + - name: Add SSH public key + vyos.rest.vyos_user: + users: + - name: alice + public_keys: + - name: alice-laptop + type: ssh-rsa + key: AAAAB3NzaC1yc2EAAAADAQABAAAB... + state: present + + - name: Delete user + vyos.rest.vyos_user: + users: + - name: alice + state: absent + + - name: Gather all users + vyos.rest.vyos_user: + 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>User 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>User 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 user 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_bgp_address_family.py b/plugins/modules/vyos_bgp_address_family.py new file mode 100644 index 0000000..65f5810 --- /dev/null +++ b/plugins/modules/vyos_bgp_address_family.py @@ -0,0 +1,631 @@ +#!/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_bgp_address_family +short_description: Manage BGP address-family configuration on VyOS devices using REST API +description: + - Manages BGP address-family configuration on VyOS devices via the REST API. + - Covers global address-family (networks, redistribution) and + per-neighbor address-family settings. + - BGP must be configured first using M(vyos.rest.vyos_bgp_global). + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: BGP address-family configuration. + type: dict + suboptions: + as_number: + description: BGP autonomous system number (required for context). + type: int + required: true + address_family: + description: Global BGP address-family settings. + type: list + elements: dict + suboptions: + afi: + description: Address family identifier. + type: str + choices: [ipv4, ipv6] + required: true + networks: + description: Networks to advertise. + type: list + elements: dict + suboptions: + prefix: + description: Network prefix. + type: str + required: true + route_map: + description: Route map to apply. + type: str + backdoor: + description: Network backdoor. + type: bool + redistribute: + description: Redistribute routes from other protocols. + type: list + elements: dict + suboptions: + protocol: + description: Protocol to redistribute. + type: str + choices: [connected, kernel, ospf, ospfv3, rip, ripng, static] + required: true + metric: + description: Metric for redistributed routes. + type: int + route_map: + description: Route map to apply. + type: str + neighbors: + description: Per-neighbor address-family settings. + type: list + elements: dict + suboptions: + neighbor_address: + description: Neighbor IP address. + type: str + required: true + address_family: + description: Address-family settings for this neighbor. + type: list + elements: dict + suboptions: + afi: + description: Address family identifier. + type: str + choices: [ipv4, ipv6] + required: true + allowas_in: + description: Accept as-path with my AS present. + type: int + attribute_unchanged: + description: BGP attributes to leave unchanged. + type: dict + suboptions: + as_path: + description: Leave as-path unchanged. + type: bool + med: + description: Leave MED unchanged. + type: bool + next_hop: + description: Leave next-hop unchanged. + type: bool + capability: + description: Advertise capability to the peer. + type: dict + suboptions: + orf: + description: ORF capability. + type: str + choices: [receive, send] + default_originate: + description: Send default route to neighbor. + type: bool + distribute_list: + description: Filter updates using access-list. + type: dict + suboptions: + import: + description: Access-list to filter inbound updates. + type: int + export: + description: Access-list to filter outbound updates. + type: int + maximum_prefix: + description: Maximum number of prefixes to accept. + type: int + nexthop_self: + description: Set next-hop to self. + type: bool + prefix_list: + description: Filter updates using prefix-list. + type: dict + suboptions: + import: + description: Prefix-list to filter inbound updates. + type: str + export: + description: Prefix-list to filter outbound updates. + type: str + route_map: + description: Route map to apply. + type: dict + suboptions: + import: + description: Route map for inbound updates. + type: str + export: + description: Route map for outbound updates. + type: str + route_reflector_client: + description: Configure as route reflector client. + type: bool + route_server_client: + description: Configure as route server client. + type: bool + soft_reconfiguration: + description: Enable soft reconfiguration inbound. + type: bool + unsuppress_map: + description: Route-map to selectively unsuppress suppressed routes. + type: str + weight: + description: Default weight for routes from this neighbor. + type: int + state: + description: + - Desired state of the BGP address-family configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces the entire BGP address-family configuration. + - C(deleted) removes BGP address-family 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). + - BGP must be configured first using M(vyos.rest.vyos_bgp_global). +""" + +EXAMPLES = r""" +- name: Merge BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + redistribute: + - protocol: connected + metric: 10 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + nexthop_self: true + - afi: ipv6 + soft_reconfiguration: true + state: merged + +- name: Delete all BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + state: deleted + +- name: Gather BGP address-family configuration + vyos.rest.vyos_bgp_address_family: + state: gathered +""" + +RETURN = r""" +before: + description: BGP address-family configuration before this module ran. + returned: always + type: dict +after: + description: BGP address-family 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 BGP address-family 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", "bgp"] +_AFI_MAP = {"ipv4": "ipv4-unicast", "ipv6": "ipv6-unicast"} +_AFI_RMAP = {"ipv4-unicast": "ipv4", "ipv6-unicast": "ipv6"} + + +def _parse_global_af(raw_afs): + if not raw_afs or not isinstance(raw_afs, dict): + return [] + result = [] + for af_key, af_data in sorted(raw_afs.items()): + afi = _AFI_RMAP.get(af_key) + if not afi: + continue + af_data = af_data or {} + entry = {"afi": afi} + + nets = af_data.get("network", {}) + if nets and isinstance(nets, dict): + entry["networks"] = [{"prefix": p} for p in sorted(nets.keys())] + + redist = af_data.get("redistribute", {}) + if redist and isinstance(redist, dict): + redist_list = [] + for proto, rdata in sorted(redist.items()): + r = {"protocol": proto} + rdata = rdata or {} + if "metric" in rdata: + r["metric"] = int(rdata["metric"]) + if "route-map" in rdata: + r["route_map"] = rdata["route-map"] + redist_list.append(r) + if redist_list: + entry["redistribute"] = redist_list + + result.append(entry) + return result + + +def _parse_neighbor_af(raw_afs): + if not raw_afs or not isinstance(raw_afs, dict): + return [] + result = [] + for af_key, af_data in sorted(raw_afs.items()): + afi = _AFI_RMAP.get(af_key) + if not afi: + continue + af_data = af_data or {} + entry = {"afi": afi} + + if "nexthop-self" in af_data: + entry["nexthop_self"] = True + if "route-reflector-client" in af_data: + entry["route_reflector_client"] = True + if "route-server-client" in af_data: + entry["route_server_client"] = True + if "default-originate" in af_data: + entry["default_originate"] = True + if "maximum-prefix" in af_data: + entry["maximum_prefix"] = int(af_data["maximum-prefix"]) + if "weight" in af_data: + entry["weight"] = int(af_data["weight"]) + if "unsuppress-map" in af_data: + entry["unsuppress_map"] = af_data["unsuppress-map"] + if "allowas-in" in af_data: + ai = af_data["allowas-in"] + if isinstance(ai, dict) and "number" in ai: + entry["allowas_in"] = int(ai["number"]) + else: + entry["allowas_in"] = 1 + + sc = af_data.get("soft-reconfiguration", {}) + if sc and "inbound" in sc: + entry["soft_reconfiguration"] = True + + rm = af_data.get("route-map", {}) + if rm: + entry["route_map"] = {} + if "import" in rm: + entry["route_map"]["import"] = rm["import"] + if "export" in rm: + entry["route_map"]["export"] = rm["export"] + + pl = af_data.get("prefix-list", {}) + if pl: + entry["prefix_list"] = {} + if "import" in pl: + entry["prefix_list"]["import"] = pl["import"] + if "export" in pl: + entry["prefix_list"]["export"] = pl["export"] + + result.append(entry) + return result + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + result = {} + + if "system-as" in raw: + result["as_number"] = int(raw["system-as"]) + + global_afs = _parse_global_af(raw.get("address-family")) + if global_afs: + result["address_family"] = global_afs + + neighbors = [] + for nb_id, nb_data in sorted((raw.get("neighbor") or {}).items()): + nb_data = nb_data or {} + nb_afs = _parse_neighbor_af(nb_data.get("address-family")) + if nb_afs: + neighbors.append({"neighbor_address": nb_id, "address_family": nb_afs}) + if neighbors: + result["neighbors"] = neighbors + + return result + + +def _global_af_cmds(af, have_af): + cmds = [] + afi = af["afi"] + af_key = _AFI_MAP[afi] + abase = _BASE + ["address-family", af_key] + have_af = have_af or {} + + want_nets = {n["prefix"]: n for n in (af.get("networks") or [])} + have_nets = {n["prefix"]: n for n in (have_af.get("networks") or [])} + for prefix in want_nets: + if prefix not in have_nets: + cmds.append(("set", abase + ["network", prefix])) + + want_redist = {r["protocol"]: r for r in (af.get("redistribute") or [])} + have_redist = {r["protocol"]: r for r in (have_af.get("redistribute") or [])} + for proto, entry in want_redist.items(): + have_entry = have_redist.get(proto, {}) + rbase = abase + ["redistribute", proto] + if proto not in have_redist: + 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("route_map") and entry["route_map"] != have_entry.get("route_map"): + cmds.append(("set", rbase + ["route-map", entry["route_map"]])) + + return cmds + + +def _neighbor_af_cmds(nb_addr, af, have_af): + cmds = [] + afi = af["afi"] + af_key = _AFI_MAP[afi] + nbase = _BASE + ["neighbor", nb_addr, "address-family", af_key] + have_af = have_af or {} + + if af.get("soft_reconfiguration") and not have_af.get("soft_reconfiguration"): + cmds.append(("set", nbase + ["soft-reconfiguration", "inbound"])) + if af.get("nexthop_self") and not have_af.get("nexthop_self"): + cmds.append(("set", nbase + ["nexthop-self"])) + if af.get("route_reflector_client") and not have_af.get("route_reflector_client"): + cmds.append(("set", nbase + ["route-reflector-client"])) + if af.get("route_server_client") and not have_af.get("route_server_client"): + cmds.append(("set", nbase + ["route-server-client"])) + if af.get("default_originate") and not have_af.get("default_originate"): + cmds.append(("set", nbase + ["default-originate"])) + if af.get("maximum_prefix") and af["maximum_prefix"] != have_af.get("maximum_prefix"): + cmds.append(("set", nbase + ["maximum-prefix", str(af["maximum_prefix"])])) + if af.get("weight") and af["weight"] != have_af.get("weight"): + cmds.append(("set", nbase + ["weight", str(af["weight"])])) + if af.get("allowas_in") and af["allowas_in"] != have_af.get("allowas_in"): + cmds.append(("set", nbase + ["allowas-in", "number", str(af["allowas_in"])])) + if af.get("unsuppress_map") and af["unsuppress_map"] != have_af.get("unsuppress_map"): + cmds.append(("set", nbase + ["unsuppress-map", af["unsuppress_map"]])) + + want_rm = af.get("route_map") or {} + have_rm = have_af.get("route_map") or {} + if want_rm.get("import") and want_rm["import"] != have_rm.get("import"): + cmds.append(("set", nbase + ["route-map", "import", want_rm["import"]])) + if want_rm.get("export") and want_rm["export"] != have_rm.get("export"): + cmds.append(("set", nbase + ["route-map", "export", want_rm["export"]])) + + want_pl = af.get("prefix_list") or {} + have_pl = have_af.get("prefix_list") or {} + if want_pl.get("import") and want_pl["import"] != have_pl.get("import"): + cmds.append(("set", nbase + ["prefix-list", "import", want_pl["import"]])) + if want_pl.get("export") and want_pl["export"] != have_pl.get("export"): + cmds.append(("set", nbase + ["prefix-list", "export", want_pl["export"]])) + + return cmds + + +def build_commands(config, have, state): + cmds = [] + config = config or {} + + if state == "deleted": + if have.get("address_family"): + cmds.append(("delete", _BASE + ["address-family"])) + for nb in have.get("neighbors") or []: + path = _BASE + ["neighbor", nb["neighbor_address"], "address-family"] + cmds.append(("delete", path)) + 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.get("address_family"): + cmds.append(("delete", _BASE + ["address-family"])) + for nb in have.get("neighbors") or []: + path = _BASE + ["neighbor", nb["neighbor_address"], "address-family"] + cmds.append(("delete", path)) + have = {} + + # global address-family + have_global_af_map = {af["afi"]: af for af in (have.get("address_family") or [])} + for af in config.get("address_family") or []: + cmds += _global_af_cmds(af, have_global_af_map.get(af["afi"])) + + # per-neighbor address-family + have_nb_map = { + n["neighbor_address"]: {af["afi"]: af for af in n.get("address_family", [])} + for n in (have.get("neighbors") or []) + } + + for nb in config.get("neighbors") or []: + nb_addr = nb["neighbor_address"] + have_nb_afs = have_nb_map.get(nb_addr, {}) + for af in nb.get("address_family") or []: + cmds += _neighbor_af_cmds(nb_addr, af, have_nb_afs.get(af["afi"])) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + as_number=dict(type="int", required=True), + address_family=dict( + type="list", + elements="dict", + options=dict( + afi=dict(type="str", choices=["ipv4", "ipv6"], required=True), + networks=dict( + type="list", + elements="dict", + options=dict( + prefix=dict(type="str", required=True), + route_map=dict(type="str"), + backdoor=dict(type="bool"), + ), + ), + redistribute=dict( + type="list", + elements="dict", + options=dict( + protocol=dict( + type="str", + required=True, + choices=[ + "connected", + "kernel", + "ospf", + "ospfv3", + "rip", + "ripng", + "static", + ], + ), + metric=dict(type="int"), + route_map=dict(type="str"), + ), + ), + ), + ), + neighbors=dict( + type="list", + elements="dict", + options=dict( + neighbor_address=dict(type="str", required=True), + address_family=dict( + type="list", + elements="dict", + options=dict( + afi=dict(type="str", choices=["ipv4", "ipv6"], required=True), + allowas_in=dict(type="int"), + default_originate=dict(type="bool"), + maximum_prefix=dict(type="int"), + nexthop_self=dict(type="bool"), + route_reflector_client=dict(type="bool"), + route_server_client=dict(type="bool"), + soft_reconfiguration=dict(type="bool"), + unsuppress_map=dict(type="str"), + weight=dict(type="int"), + attribute_unchanged=dict( + type="dict", + options=dict( + as_path=dict(type="bool"), + med=dict(type="bool"), + next_hop=dict(type="bool"), + ), + ), + capability=dict( + type="dict", + options=dict( + orf=dict(type="str", choices=["receive", "send"]), + ), + ), + distribute_list=dict( + type="dict", + options=dict( + **{ + "import": dict(type="int"), + "export": dict(type="int"), + }, + ), + ), + prefix_list=dict( + type="dict", + options=dict( + **{ + "import": dict(type="str"), + "export": dict(type="str"), + }, + ), + ), + route_map=dict( + type="dict", + options=dict( + **{ + "import": dict(type="str"), + "export": 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_bgp_global.py b/plugins/modules/vyos_bgp_global.py new file mode 100644 index 0000000..a05e3b0 --- /dev/null +++ b/plugins/modules/vyos_bgp_global.py @@ -0,0 +1,572 @@ +#!/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_bgp_global +short_description: Manage BGP global configuration on VyOS devices using REST API +description: + - Manages BGP global configuration on VyOS devices via the REST API. + - Covers system AS, parameters, neighbors, and peer-groups. + - For per-neighbor address-family configuration use M(vyos.rest.vyos_bgp_address_family). + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: BGP global configuration. + type: dict + suboptions: + as_number: + description: BGP autonomous system number. + type: int + required: true + parameters: + description: BGP global parameters. + type: dict + suboptions: + router_id: + description: BGP router ID. + type: str + confederation: + description: AS confederation parameters. + type: dict + suboptions: + identifier: + description: Confederation AS identifier. + type: int + peers: + description: Peer ASs in confederation. + type: list + elements: int + bestpath: + description: BGP bestpath parameters. + type: dict + suboptions: + as_path: + description: AS-path attribute comparison. + type: str + choices: [confed, ignore, multipath-relax] + graceful_restart: + description: Enable graceful restart. + type: bool + log_neighbor_changes: + description: Log neighbor up/down changes. + type: bool + no_ipv4_unicast: + description: Disable IPv4 unicast default. + type: bool + neighbors: + description: BGP neighbors. + type: list + elements: dict + suboptions: + neighbor_address: + description: Neighbor IP address. + type: str + required: true + remote_as: + description: Neighbor AS number. + type: int + description: + description: Neighbor description. + type: str + disable_connected_check: + description: Disable connected route check. + type: bool + ebgp_multihop: + description: EBGP multihop TTL. + type: int + local_as: + description: Local AS number. + type: int + password: + description: MD5 password for neighbor. + type: str + peer_group: + description: Peer group name. + type: str + shutdown: + description: Shutdown neighbor. + type: bool + timers: + description: Neighbor timers. + type: dict + suboptions: + holdtime: + description: Hold time in seconds. + type: int + keepalive: + description: Keepalive interval in seconds. + type: int + update_source: + description: Source interface/IP for updates. + type: str + peer_groups: + description: BGP peer groups. + type: list + elements: dict + suboptions: + peer_group: + description: Peer group name. + type: str + required: true + remote_as: + description: Peer group AS number. + type: int + description: + description: Peer group description. + type: str + ebgp_multihop: + description: EBGP multihop TTL. + type: int + password: + description: MD5 password. + type: str + timers: + description: Peer group timers. + type: dict + suboptions: + holdtime: + description: Hold time in seconds. + type: int + keepalive: + description: Keepalive interval in seconds. + type: int + update_source: + description: Source interface/IP for updates. + type: str + state: + description: + - Desired state of the BGP global configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces the entire BGP configuration. + - C(deleted) removes BGP 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). + - BGP system-as must be defined before any other BGP configuration. +""" + +EXAMPLES = r""" +- name: Merge BGP global configuration + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + description: peer1 + timers: + holdtime: 30 + keepalive: 10 + peer_groups: + - peer_group: PG1 + remote_as: 65002 + state: merged + +- name: Delete BGP configuration + vyos.rest.vyos_bgp_global: + state: deleted + +- name: Gather BGP global configuration + vyos.rest.vyos_bgp_global: + state: gathered +""" + +RETURN = r""" +before: + description: BGP configuration before this module ran. + returned: always + type: dict +after: + description: BGP 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 BGP 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", "bgp"] + + +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 "log-neighbor-changes" in raw: + result["log_neighbor_changes"] = True + if "no-ipv4-unicast" in raw: + result["no_ipv4_unicast"] = True + if "graceful-restart" in raw: + result["graceful_restart"] = True + bp = raw.get("bestpath", {}) or {} + if bp: + bestpath = {} + if "as-path" in bp: + bestpath["as_path"] = bp["as-path"] + if bestpath: + result["bestpath"] = bestpath + conf = raw.get("confederation", {}) or {} + if conf: + confederation = {} + if "identifier" in conf: + confederation["identifier"] = int(conf["identifier"]) + if "peers" in conf: + peers = conf["peers"] + if isinstance(peers, list): + confederation["peers"] = [int(p) for p in peers] + else: + confederation["peers"] = [int(peers)] + if confederation: + result["confederation"] = confederation + return result + + +def _parse_neighbor(nb_id, data): + nb = {"neighbor_address": nb_id} + data = data or {} + if "remote-as" in data: + nb["remote_as"] = int(data["remote-as"]) + if "description" in data: + nb["description"] = data["description"] + if "ebgp-multihop" in data: + nb["ebgp_multihop"] = int(data["ebgp-multihop"]) + if "local-as" in data: + nb["local_as"] = int(data["local-as"]) + if "password" in data: + nb["password"] = data["password"] + if "peer-group" in data: + nb["peer_group"] = data["peer-group"] + if "shutdown" in data: + nb["shutdown"] = True + if "update-source" in data: + nb["update_source"] = data["update-source"] + if "disable-connected-check" in data: + nb["disable_connected_check"] = True + timers = data.get("timers", {}) or {} + if timers: + t = {} + if "holdtime" in timers: + t["holdtime"] = int(timers["holdtime"]) + if "keepalive" in timers: + t["keepalive"] = int(timers["keepalive"]) + if t: + nb["timers"] = t + return nb + + +def _parse_peer_group(pg_name, data): + pg = {"peer_group": pg_name} + data = data or {} + if "remote-as" in data: + pg["remote_as"] = int(data["remote-as"]) + if "description" in data: + pg["description"] = data["description"] + if "ebgp-multihop" in data: + pg["ebgp_multihop"] = int(data["ebgp-multihop"]) + if "password" in data: + pg["password"] = data["password"] + if "update-source" in data: + pg["update_source"] = data["update-source"] + timers = data.get("timers", {}) or {} + if timers: + t = {} + if "holdtime" in timers: + t["holdtime"] = int(timers["holdtime"]) + if "keepalive" in timers: + t["keepalive"] = int(timers["keepalive"]) + if t: + pg["timers"] = t + return pg + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + result = {} + + if "system-as" in raw: + result["as_number"] = int(raw["system-as"]) + + params = _parse_parameters(raw.get("parameters")) + if params: + result["parameters"] = params + + neighbors = [] + for nb_id, data in sorted((raw.get("neighbor") or {}).items()): + neighbors.append(_parse_neighbor(nb_id, data)) + if neighbors: + result["neighbors"] = neighbors + + peer_groups = [] + for pg_name, data in sorted((raw.get("peer-group") or {}).items()): + peer_groups.append(_parse_peer_group(pg_name, data)) + if peer_groups: + result["peer_groups"] = peer_groups + + return result + + +def _neighbor_cmds(nb, have_nb): + cmds = [] + nb_addr = nb["neighbor_address"] + nbase = _BASE + ["neighbor", nb_addr] + have_nb = have_nb or {} + + if nb.get("remote_as") and nb["remote_as"] != have_nb.get("remote_as"): + cmds.append(("set", nbase + ["remote-as", str(nb["remote_as"])])) + if nb.get("description") and nb["description"] != have_nb.get("description"): + cmds.append(("set", nbase + ["description", nb["description"]])) + if nb.get("ebgp_multihop") and nb["ebgp_multihop"] != have_nb.get("ebgp_multihop"): + cmds.append(("set", nbase + ["ebgp-multihop", str(nb["ebgp_multihop"])])) + if nb.get("local_as") and nb["local_as"] != have_nb.get("local_as"): + cmds.append(("set", nbase + ["local-as", str(nb["local_as"])])) + if nb.get("password") and nb["password"] != have_nb.get("password"): + cmds.append(("set", nbase + ["password", nb["password"]])) + if nb.get("peer_group") and nb["peer_group"] != have_nb.get("peer_group"): + cmds.append(("set", nbase + ["peer-group", nb["peer_group"]])) + if nb.get("update_source") and nb["update_source"] != have_nb.get("update_source"): + cmds.append(("set", nbase + ["update-source", nb["update_source"]])) + if nb.get("shutdown") and not have_nb.get("shutdown"): + cmds.append(("set", nbase + ["shutdown"])) + if nb.get("disable_connected_check") and not have_nb.get("disable_connected_check"): + cmds.append(("set", nbase + ["disable-connected-check"])) + + want_t = nb.get("timers") or {} + have_t = have_nb.get("timers") or {} + if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"): + cmds.append(("set", nbase + ["timers", "holdtime", str(want_t["holdtime"])])) + if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"): + cmds.append(("set", nbase + ["timers", "keepalive", str(want_t["keepalive"])])) + + return cmds + + +def _peer_group_cmds(pg, have_pg): + cmds = [] + pg_name = pg["peer_group"] + pbase = _BASE + ["peer-group", pg_name] + have_pg = have_pg or {} + + if pg.get("remote_as") and pg["remote_as"] != have_pg.get("remote_as"): + cmds.append(("set", pbase + ["remote-as", str(pg["remote_as"])])) + if pg.get("description") and pg["description"] != have_pg.get("description"): + cmds.append(("set", pbase + ["description", pg["description"]])) + if pg.get("ebgp_multihop") and pg["ebgp_multihop"] != have_pg.get("ebgp_multihop"): + cmds.append(("set", pbase + ["ebgp-multihop", str(pg["ebgp_multihop"])])) + if pg.get("password") and pg["password"] != have_pg.get("password"): + cmds.append(("set", pbase + ["password", pg["password"]])) + if pg.get("update_source") and pg["update_source"] != have_pg.get("update_source"): + cmds.append(("set", pbase + ["update-source", pg["update_source"]])) + + want_t = pg.get("timers") or {} + have_t = have_pg.get("timers") or {} + if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"): + cmds.append(("set", pbase + ["timers", "holdtime", str(want_t["holdtime"])])) + if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"): + cmds.append(("set", pbase + ["timers", "keepalive", str(want_t["keepalive"])])) + + 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 {} + + # system-as — must be first + if config.get("as_number") and config["as_number"] != have.get("as_number"): + cmds.append(("set", _BASE + ["system-as", str(config["as_number"])])) + + # parameters + params = config.get("parameters") or {} + have_params = have.get("parameters") or {} + if params.get("router_id") and params["router_id"] != have_params.get("router_id"): + cmds.append(("set", _BASE + ["parameters", "router-id", params["router_id"]])) + if params.get("log_neighbor_changes") and not have_params.get("log_neighbor_changes"): + cmds.append(("set", _BASE + ["parameters", "log-neighbor-changes"])) + if params.get("no_ipv4_unicast") and not have_params.get("no_ipv4_unicast"): + cmds.append(("set", _BASE + ["parameters", "no-ipv4-unicast"])) + if params.get("graceful_restart") and not have_params.get("graceful_restart"): + cmds.append(("set", _BASE + ["parameters", "graceful-restart"])) + bp = params.get("bestpath") or {} + have_bp = have_params.get("bestpath") or {} + if bp.get("as_path") and bp["as_path"] != have_bp.get("as_path"): + cmds.append(("set", _BASE + ["parameters", "bestpath", "as-path", bp["as_path"]])) + + # neighbors + have_nb_map = {n["neighbor_address"]: n for n in (have.get("neighbors") or [])} + for nb in config.get("neighbors") or []: + cmds += _neighbor_cmds(nb, have_nb_map.get(nb["neighbor_address"])) + + # peer_groups + have_pg_map = {p["peer_group"]: p for p in (have.get("peer_groups") or [])} + for pg in config.get("peer_groups") or []: + cmds += _peer_group_cmds(pg, have_pg_map.get(pg["peer_group"])) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + as_number=dict(type="int", required=True), + parameters=dict( + type="dict", + options=dict( + router_id=dict(type="str"), + log_neighbor_changes=dict(type="bool"), + no_ipv4_unicast=dict(type="bool"), + graceful_restart=dict(type="bool"), + bestpath=dict( + type="dict", + options=dict( + as_path=dict( + type="str", + choices=["confed", "ignore", "multipath-relax"], + ), + ), + ), + confederation=dict( + type="dict", + options=dict( + identifier=dict(type="int"), + peers=dict(type="list", elements="int"), + ), + ), + ), + ), + neighbors=dict( + type="list", + elements="dict", + options=dict( + neighbor_address=dict(type="str", required=True), + remote_as=dict(type="int"), + description=dict(type="str"), + disable_connected_check=dict(type="bool"), + ebgp_multihop=dict(type="int"), + local_as=dict(type="int"), + password=dict(type="str", no_log=True), + peer_group=dict(type="str"), + shutdown=dict(type="bool"), + timers=dict( + type="dict", + options=dict( + holdtime=dict(type="int"), + keepalive=dict(type="int"), + ), + ), + update_source=dict(type="str"), + ), + ), + peer_groups=dict( + type="list", + elements="dict", + options=dict( + peer_group=dict(type="str", required=True), + remote_as=dict(type="int"), + description=dict(type="str"), + ebgp_multihop=dict(type="int"), + password=dict(type="str", no_log=True), + timers=dict( + type="dict", + options=dict( + holdtime=dict(type="int"), + keepalive=dict(type="int"), + ), + ), + update_source=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_facts.py b/plugins/modules/vyos_facts.py new file mode 100644 index 0000000..b75522d --- /dev/null +++ b/plugins/modules/vyos_facts.py @@ -0,0 +1,287 @@ +#!/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_facts +short_description: Get facts about VyOS devices using REST API +description: + - Collects facts from VyOS devices via the REST API. + - Returns structured facts under the C(ansible_facts) key. + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected to + a given subset. Possible values for this argument include C(all), + C(default), C(config), C(interfaces), C(hostname), C(users), + C(bgp), C(ospf), C(ntp), C(snmp) and C(logging). + - Specify a list of values to include a larger subset. Use the + exclamation mark (C(!)) before a value to exclude it. Values + C(all) and C(default) cannot be combined with each other or with + negation. + type: list + elements: str + default: ['default'] + gather_network_resources: + description: + - When supplied, this argument will restrict the facts collected to + a given subset. Possible values include the resource module names. + - This argument is not currently used. + type: list + elements: str +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). + - Only configuration facts are available via the REST API. + Operational state (interface counters, BGP neighbors) is not supported. +""" + +EXAMPLES = r""" +- name: Gather all facts + vyos.rest.vyos_facts: + gather_subset: all + +- name: Gather default facts + vyos.rest.vyos_facts: + +- name: Gather interface and hostname facts only + vyos.rest.vyos_facts: + gather_subset: + - interfaces + - hostname + +- name: Gather all except config + vyos.rest.vyos_facts: + gather_subset: + - all + - '!config' +""" + +RETURN = r""" +ansible_facts: + description: Facts collected from the device. + returned: always + type: dict + contains: + vyos_hostname: + description: Device hostname. + type: str + vyos_config: + description: Full device configuration as structured data. + type: dict + vyos_interfaces: + description: Interface configuration. + type: dict + vyos_users: + description: User accounts (without passwords). + type: list + vyos_bgp: + description: BGP configuration. + type: dict + vyos_ospf: + description: OSPFv2 configuration. + type: dict + vyos_ospfv3: + description: OSPFv3 configuration. + type: dict + vyos_ntp: + description: NTP configuration. + type: dict + vyos_snmp: + description: SNMP configuration. + type: dict + vyos_logging: + description: Logging configuration. + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +VALID_SUBSETS = frozenset( + [ + "all", + "default", + "config", + "interfaces", + "hostname", + "users", + "bgp", + "ospf", + "ospfv3", + "ntp", + "snmp", + "logging", + ], +) + +DEFAULT_SUBSETS = frozenset(["hostname", "interfaces"]) + + +def _get_config(vyos, path): + try: + result = vyos.get_config(path) + return result or {} + except Exception: + return {} + + +def gather_hostname(vyos): + raw = _get_config(vyos, ["system"]) + return raw.get("host-name", "") + + +def gather_config(vyos): + return _get_config(vyos, []) + + +def gather_interfaces(vyos): + return _get_config(vyos, ["interfaces"]) + + +def gather_users(vyos): + raw = _get_config(vyos, ["system", "login", "user"]) + if not raw or not isinstance(raw, dict): + return [] + raw = raw.get("user", raw) + users = [] + for username, data in sorted(raw.items()): + user = {"name": username} + data = data or {} + if data.get("full-name"): + user["full_name"] = data["full-name"] + auth = data.get("authentication", {}) or {} + pub_keys = auth.get("public-keys", {}) or {} + if pub_keys: + user["public_keys"] = list(pub_keys.keys()) + users.append(user) + return users + + +def gather_bgp(vyos): + return _get_config(vyos, ["protocols", "bgp"]) + + +def gather_ospf(vyos): + return _get_config(vyos, ["protocols", "ospf"]) + + +def gather_ospfv3(vyos): + return _get_config(vyos, ["protocols", "ospfv3"]) + + +def gather_ntp(vyos): + return _get_config(vyos, ["service", "ntp"]) + + +def gather_snmp(vyos): + return _get_config(vyos, ["service", "snmp"]) + + +def gather_logging(vyos): + return _get_config(vyos, ["system", "syslog"]) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + gather_subset=dict( + type="list", + elements="str", + default=["default"], + ), + gather_network_resources=dict( + type="list", + elements="str", + ), + ), + supports_check_mode=True, + ) + + vyos = VyOSModule(module) + gather_subset = module.params["gather_subset"] + + # Normalize subset + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset.startswith("!"): + exclude = subset[1:] + if exclude not in VALID_SUBSETS: + module.fail_json(msg="Invalid subset: %s" % exclude) + exclude_subsets.add(exclude) + elif subset == "all": + runable_subsets.update(VALID_SUBSETS - {"all", "default"}) + elif subset == "default": + runable_subsets.update(DEFAULT_SUBSETS) + elif subset in VALID_SUBSETS: + runable_subsets.add(subset) + else: + module.fail_json(msg="Invalid subset: %s" % subset) + + if not runable_subsets: + runable_subsets.update(DEFAULT_SUBSETS) + + runable_subsets -= exclude_subsets + runable_subsets -= {"all", "default"} + + facts = {} + + if "hostname" in runable_subsets: + facts["vyos_hostname"] = gather_hostname(vyos) + + if "config" in runable_subsets: + facts["vyos_config"] = gather_config(vyos) + + if "interfaces" in runable_subsets: + facts["vyos_interfaces"] = gather_interfaces(vyos) + + if "users" in runable_subsets: + facts["vyos_users"] = gather_users(vyos) + + if "bgp" in runable_subsets: + bgp = gather_bgp(vyos) + if bgp: + facts["vyos_bgp"] = bgp + + if "ospf" in runable_subsets: + ospf = gather_ospf(vyos) + if ospf: + facts["vyos_ospf"] = ospf + + if "ospfv3" in runable_subsets: + ospfv3 = gather_ospfv3(vyos) + if ospfv3: + facts["vyos_ospfv3"] = ospfv3 + + if "ntp" in runable_subsets: + ntp = gather_ntp(vyos) + if ntp: + facts["vyos_ntp"] = ntp + + if "snmp" in runable_subsets: + snmp = gather_snmp(vyos) + if snmp: + facts["vyos_snmp"] = snmp + + if "logging" in runable_subsets: + logging = gather_logging(vyos) + if logging: + facts["vyos_logging"] = logging + + module.exit_json(ansible_facts=facts) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/vyos_firewall_global.py b/plugins/modules/vyos_firewall_global.py new file mode 100644 index 0000000..66cdabc --- /dev/null +++ b/plugins/modules/vyos_firewall_global.py @@ -0,0 +1,402 @@ +#!/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_firewall_global +short_description: Manage global firewall configuration on VyOS devices using REST API +description: + - Manages global firewall group configuration on VyOS devices via the REST API. + - Covers address-groups, network-groups, port-groups, interface-groups, + and IPv6 network-groups. + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: Global firewall configuration. + type: dict + suboptions: + group: + description: Firewall groups. + type: dict + suboptions: + address_group: + description: IPv4 address groups. + type: list + elements: dict + suboptions: + name: + description: Group name. + type: str + required: true + description: + description: Group description. + type: str + address: + description: IP addresses or ranges in the group. + type: list + elements: str + network_group: + description: IPv4 network groups. + type: list + elements: dict + suboptions: + name: + description: Group name. + type: str + required: true + description: + description: Group description. + type: str + network: + description: Network prefixes in the group. + type: list + elements: str + port_group: + description: Port groups. + type: list + elements: dict + suboptions: + name: + description: Group name. + type: str + required: true + description: + description: Group description. + type: str + port: + description: Ports or port ranges in the group. + type: list + elements: str + interface_group: + description: Interface groups. + type: list + elements: dict + suboptions: + name: + description: Group name. + type: str + required: true + description: + description: Group description. + type: str + interface: + description: Interfaces in the group. + type: list + elements: str + ipv6_network_group: + description: IPv6 network groups. + type: list + elements: dict + suboptions: + name: + description: Group name. + type: str + required: true + description: + description: Group description. + type: str + network: + description: IPv6 network prefixes in the group. + type: list + elements: str + state: + description: + - Desired state of the firewall global configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces the entire firewall global configuration. + - C(deleted) removes firewall global 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 firewall global configuration + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + description: Web servers + address: + - 192.168.1.10 + - 192.168.1.11 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + port_group: + - name: WEB-PORTS + port: + - "80" + - "443" + interface_group: + - name: LAN-IFACES + interface: + - eth1 + - eth2 + ipv6_network_group: + - name: IPV6-LAN + network: + - "2001:db8::/32" + state: merged + +- name: Delete all firewall global configuration + vyos.rest.vyos_firewall_global: + state: deleted + +- name: Gather firewall global configuration + vyos.rest.vyos_firewall_global: + state: gathered +""" + +RETURN = r""" +before: + description: Firewall global configuration before this module ran. + returned: always + type: dict +after: + description: Firewall global 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 firewall global 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 = ["firewall", "group"] + +# Map argspec key -> API key, value key +_GROUP_TYPES = { + "address_group": ("address-group", "address"), + "network_group": ("network-group", "network"), + "port_group": ("port-group", "port"), + "interface_group": ("interface-group", "interface"), + "ipv6_network_group": ("ipv6-network-group", "network"), +} + + +def _parse_group_type(raw, val_key): + """Parse a group dict from API raw data.""" + if not raw or not isinstance(raw, dict): + return [] + result = [] + for name, data in sorted(raw.items()): + entry = {"name": name} + data = data or {} + if data.get("description"): + entry["description"] = data["description"] + val = data.get(val_key) + if val is not None: + if isinstance(val, list): + entry[val_key.replace("-", "_")] = val + elif isinstance(val, str): + entry[val_key.replace("-", "_")] = [val] + elif isinstance(val, dict): + entry[val_key.replace("-", "_")] = list(val.keys()) + result.append(entry) + return result + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + result = {"group": {}} + + for arg_key, (api_key, val_key) in _GROUP_TYPES.items(): + groups = _parse_group_type(raw.get(api_key), val_key) + if groups: + result["group"][arg_key] = groups + + if not result["group"]: + return {} + return result + + +def _group_cmds(arg_key, groups, have_groups, state): + cmds = [] + api_key, val_key = _GROUP_TYPES[arg_key] + have_map = {g["name"]: g for g in (have_groups or [])} + want_map = {g["name"]: g for g in (groups or [])} + + if state == "replaced": + for name in set(have_map) - set(want_map): + cmds.append(("delete", _BASE + [api_key, name])) + + for name, group in want_map.items(): + have_group = have_map.get(name, {}) + gbase = _BASE + [api_key, name] + + if group.get("description") and group["description"] != have_group.get("description"): + cmds.append(("set", gbase + ["description", group["description"]])) + + # normalize val_key for argspec (underscores) + arg_val_key = val_key.replace("-", "_") + want_vals = set(group.get(arg_val_key) or []) + have_vals = set(have_group.get(arg_val_key) or []) + + for val in want_vals - have_vals: + cmds.append(("set", gbase + [val_key, val])) + + if state == "replaced": + for val in have_vals - want_vals: + cmds.append(("delete", gbase + [val_key, val])) + + return cmds + + +def build_commands(config, have, state): + cmds = [] + + if state == "deleted": + if have: + cmds.append(("delete", _BASE)) + return cmds + + if state == "replaced": + # Check if anything differs + would_set = build_commands(config, {}, "merged") + have_set = build_commands(have, {}, "merged") + if would_set == have_set: + return [] + + config = config or {} + want_group = config.get("group") or {} + have_group = have.get("group") or {} + + for arg_key in _GROUP_TYPES: + want_groups = want_group.get(arg_key) or [] + have_groups = have_group.get(arg_key) or [] + if want_groups or (state == "replaced" and have_groups): + cmds += _group_cmds(arg_key, want_groups, have_groups, state) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + group=dict( + type="dict", + options=dict( + address_group=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + address=dict(type="list", elements="str"), + ), + ), + network_group=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + network=dict(type="list", elements="str"), + ), + ), + port_group=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + port=dict(type="list", elements="str"), + ), + ), + interface_group=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + interface=dict(type="list", elements="str"), + ), + ), + ipv6_network_group=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + description=dict(type="str"), + network=dict(type="list", elements="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_firewall_interfaces.py b/plugins/modules/vyos_firewall_interfaces.py new file mode 100644 index 0000000..8769f54 --- /dev/null +++ b/plugins/modules/vyos_firewall_interfaces.py @@ -0,0 +1,473 @@ +#!/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_firewall_interfaces +short_description: Manage firewall hook filters on VyOS devices using REST API +description: + - Manages firewall hook filter configuration on VyOS devices via the REST API. + - In VyOS 1.5+, firewall hook filters (input/output/forward) replace the + per-interface firewall assignments used in VyOS 1.4. + - Hook filters apply globally to all traffic traversing that hook point. + - Uses REST API (C(connection=httpapi)) instead of CLI. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: Firewall hook filter configuration. + type: list + elements: dict + suboptions: + afi: + description: Address family. + type: str + choices: [ipv4, ipv6] + required: true + hooks: + description: Hook filter configurations for this address family. + type: list + elements: dict + suboptions: + hook: + description: Netfilter hook point. + type: str + choices: [input, output, forward] + required: true + default_action: + description: Default action when no rule matches. + type: str + choices: [accept, drop, reject] + description: + description: Filter description. + type: str + rules: + description: Rules in this hook filter. + type: list + elements: dict + suboptions: + number: + description: Rule number. + type: int + required: true + action: + description: Rule action. + type: str + choices: [accept, drop, reject, return, queue, continue] + description: + description: Rule description. + type: str + disable: + description: Disable this rule. + type: bool + protocol: + description: Protocol to match. + type: str + state: + description: Connection state to match. + type: str + choices: [established, invalid, new, related] + log: + description: Enable logging. + type: bool + source: + description: Source match criteria. + type: dict + suboptions: + address: + description: Source IP address or prefix. + type: str + port: + description: Source port or range. + type: str + destination: + description: Destination match criteria. + type: dict + suboptions: + address: + description: Destination IP address or prefix. + type: str + port: + description: Destination port or range. + type: str + state: + description: + - Desired state of the firewall hook filter configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces hook filter config for named hooks in config. + - C(overridden) replaces all firewall hook filter config. + - C(deleted) removes firewall hook filter 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). + - In VyOS 1.5+, hook filters apply globally rather than per-interface. + Use named rule sets (M(vyos.rest.vyos_firewall_rules)) for more granular + per-traffic control. +""" + +EXAMPLES = r""" +- name: Merge firewall hook filter configuration + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + - number: 20 + action: drop + state: invalid + - hook: forward + default_action: accept + - hook: output + default_action: accept + - afi: ipv6 + hooks: + - hook: input + default_action: accept + state: merged + +- name: Delete all firewall hook filter configuration + vyos.rest.vyos_firewall_interfaces: + state: deleted + +- name: Gather firewall hook filter configuration + vyos.rest.vyos_firewall_interfaces: + state: gathered +""" + +RETURN = r""" +before: + description: Firewall hook filter configuration before this module ran. + returned: always + type: list +after: + description: Firewall hook filter 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 firewall hook filter 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 + + +_BASE = ["firewall"] +_AFIS = ["ipv4", "ipv6"] +_HOOKS = ["input", "output", "forward"] + + +def _parse_rule(rule_num, data): + rule = {"number": int(rule_num)} + data = data or {} + if "action" in data: + rule["action"] = data["action"] + if "description" in data: + rule["description"] = data["description"] + if "disable" in data: + rule["disable"] = True + if "protocol" in data: + rule["protocol"] = data["protocol"] + if "state" in data: + rule["state"] = data["state"] + if "log" in data: + rule["log"] = True + + for endpoint in ["source", "destination"]: + ep = data.get(endpoint, {}) or {} + if ep: + rule[endpoint] = {} + if "address" in ep: + rule[endpoint]["address"] = ep["address"] + if "port" in ep: + rule[endpoint]["port"] = ep["port"] + + return rule + + +def _parse_hook_filter(hook, data): + entry = {"hook": hook} + data = data or {} + filter_data = data.get("filter", {}) or {} + if "default-action" in filter_data: + entry["default_action"] = filter_data["default-action"] + if "description" in filter_data: + entry["description"] = filter_data["description"] + rules_raw = filter_data.get("rule", {}) or {} + if rules_raw and isinstance(rules_raw, dict): + rules = [ + _parse_rule(num, rdata) + for num, rdata in sorted( + rules_raw.items(), + key=lambda x: int(x[0]), + ) + ] + if rules: + entry["rules"] = rules + return entry + + +def get_running_config(vyos): + result = [] + for afi in _AFIS: + raw = vyos.get_config(_BASE + [afi]) + if not raw or not isinstance(raw, dict): + continue + hooks = [] + for hook in _HOOKS: + if hook in raw: + parsed = _parse_hook_filter(hook, raw[hook]) + if len(parsed) > 1: # more than just hook key + hooks.append(parsed) + if hooks: + result.append({"afi": afi, "hooks": hooks}) + return result + + +def _rule_cmds(afi, hook, rule, have_rule): + cmds = [] + rbase = _BASE + [afi, hook, "filter", "rule", str(rule["number"])] + have_rule = have_rule or {} + + if rule.get("action") and rule["action"] != have_rule.get("action"): + cmds.append(("set", rbase + ["action", rule["action"]])) + if rule.get("description") and rule["description"] != have_rule.get("description"): + cmds.append(("set", rbase + ["description", rule["description"]])) + if rule.get("disable") and not have_rule.get("disable"): + cmds.append(("set", rbase + ["disable"])) + if rule.get("protocol") and rule["protocol"] != have_rule.get("protocol"): + cmds.append(("set", rbase + ["protocol", rule["protocol"]])) + if rule.get("state") and rule["state"] != have_rule.get("state"): + cmds.append(("set", rbase + ["state", rule["state"]])) + if rule.get("log") and not have_rule.get("log"): + cmds.append(("set", rbase + ["log"])) + + for endpoint in ["source", "destination"]: + want_ep = rule.get(endpoint) or {} + have_ep = have_rule.get(endpoint) or {} + if want_ep.get("address") and want_ep["address"] != have_ep.get("address"): + cmds.append(("set", rbase + [endpoint, "address", want_ep["address"]])) + if want_ep.get("port") and want_ep["port"] != have_ep.get("port"): + cmds.append(("set", rbase + [endpoint, "port", str(want_ep["port"])])) + + return cmds + + +def _hook_cmds(afi, hook_entry, have_hook, state): + cmds = [] + hook = hook_entry["hook"] + hbase = _BASE + [afi, hook, "filter"] + have_hook = have_hook or {} + + if hook_entry.get("default_action") and hook_entry["default_action"] != have_hook.get( + "default_action", + ): + cmds.append(("set", hbase + ["default-action", hook_entry["default_action"]])) + if hook_entry.get("description") and hook_entry["description"] != have_hook.get("description"): + cmds.append(("set", hbase + ["description", hook_entry["description"]])) + + have_rules = {r["number"]: r for r in (have_hook.get("rules") or [])} + want_rules = {r["number"]: r for r in (hook_entry.get("rules") or [])} + + if state == "replaced": + for num in set(have_rules) - set(want_rules): + cmds.append(("delete", hbase + ["rule", str(num)])) + + for num, rule in want_rules.items(): + cmds += _rule_cmds(afi, hook, rule, have_rules.get(num)) + + return cmds + + +def build_commands(config, have_list, state): + cmds = [] + + if state == "deleted": + if not config: + if have_list: + for entry in have_list: + afi = entry["afi"] + for hook_entry in entry.get("hooks", []): + cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"])) + else: + have_map = {(e["afi"], h["hook"]): h for e in have_list for h in e.get("hooks", [])} + for entry in config: + afi = entry["afi"] + for hook_entry in entry.get("hooks") or []: + if (afi, hook_entry["hook"]) in have_map: + cmds.append(("delete", _BASE + [afi, hook_entry["hook"], "filter"])) + return cmds + + have_map = {e["afi"]: {h["hook"]: h for h in e.get("hooks", [])} for e in have_list} + + if state == "overridden": + want_keys = {(e["afi"], h["hook"]) for e in (config or []) for h in e.get("hooks", [])} + for e in have_list: + for h in e.get("hooks", []): + if (e["afi"], h["hook"]) not in want_keys: + cmds.append(("delete", _BASE + [e["afi"], h["hook"], "filter"])) + + for entry in config or []: + afi = entry["afi"] + have_afi = have_map.get(afi, {}) + + for hook_entry in entry.get("hooks") or []: + hook = hook_entry["hook"] + have_hook = have_afi.get(hook) + + if state == "replaced" and have_hook: + want_cmds = _hook_cmds(afi, hook_entry, {}, "merged") + have_hook_entry = { + "hook": hook, + "default_action": have_hook.get("default_action"), + "rules": have_hook.get("rules", []), + } + have_cmds = _hook_cmds(afi, have_hook_entry, {}, "merged") + if want_cmds != have_cmds: + cmds.append(("delete", _BASE + [afi, hook, "filter"])) + have_hook = None + + effective_state = state if state not in ("replaced", "overridden") else "merged" + cmds += _hook_cmds(afi, hook_entry, have_hook, effective_state) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + afi=dict(type="str", choices=["ipv4", "ipv6"], required=True), + hooks=dict( + type="list", + elements="dict", + options=dict( + hook=dict( + type="str", + choices=["input", "output", "forward"], + required=True, + ), + default_action=dict( + type="str", + choices=["accept", "drop", "reject"], + ), + description=dict(type="str"), + rules=dict( + type="list", + elements="dict", + options=dict( + number=dict(type="int", required=True), + action=dict( + type="str", + choices=[ + "accept", + "drop", + "reject", + "return", + "queue", + "continue", + ], + ), + description=dict(type="str"), + disable=dict(type="bool"), + protocol=dict(type="str"), + state=dict( + type="str", + choices=[ + "established", + "invalid", + "new", + "related", + ], + ), + log=dict(type="bool"), + source=dict( + type="dict", + options=dict( + address=dict(type="str"), + port=dict(type="str"), + ), + ), + destination=dict( + type="dict", + options=dict( + address=dict(type="str"), + port=dict(type="str"), + ), + ), + ), + ), + ), + ), + ), + ), + 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_firewall_rules.py b/plugins/modules/vyos_firewall_rules.py new file mode 100644 index 0000000..a1c7c01 --- /dev/null +++ b/plugins/modules/vyos_firewall_rules.py @@ -0,0 +1,546 @@ +#!/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_firewall_rules +short_description: Manage firewall rule sets on VyOS devices using REST API +description: + - Manages named firewall rule sets on VyOS devices via the REST API. + - Supports both IPv4 (C(ipv4)) and IPv6 (C(ipv6)) rule sets. + - Uses REST API (C(connection=httpapi)) instead of CLI. + - In VyOS 1.5+, firewall uses named rule sets under C(firewall.ipv4.name) + and C(firewall.ipv6.name). +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + config: + description: Firewall rule set configuration. + type: list + elements: dict + suboptions: + afi: + description: Address family. + type: str + choices: [ipv4, ipv6] + required: true + rule_sets: + description: Named rule sets for this address family. + type: list + elements: dict + suboptions: + name: + description: Rule set name. + type: str + required: true + default_action: + description: Default action when no rule matches. + type: str + choices: [accept, drop, reject] + description: + description: Rule set description. + type: str + rules: + description: Firewall rules in this rule set. + type: list + elements: dict + suboptions: + number: + description: Rule number. + type: int + required: true + action: + description: Rule action. + type: str + choices: [accept, drop, reject, return, queue, continue] + description: + description: Rule description. + type: str + disable: + description: Disable this rule. + type: bool + protocol: + description: Protocol to match. + type: str + state: + description: Connection state to match. + type: str + choices: [established, invalid, new, related] + source: + description: Source match criteria. + type: dict + suboptions: + address: + description: Source IP address or prefix. + type: str + group: + description: Source group name. + type: str + port: + description: Source port or range. + type: str + destination: + description: Destination match criteria. + type: dict + suboptions: + address: + description: Destination IP address or prefix. + type: str + group: + description: Destination group name. + type: str + port: + description: Destination port or range. + type: str + log: + description: Enable logging for this rule. + type: bool + icmp: + description: ICMP type/code to match. + type: dict + suboptions: + type: + description: ICMP type. + type: int + code: + description: ICMP code. + type: int + state: + description: + - Desired state of the firewall rules configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces rule sets for named rule sets in config. + - C(overridden) replaces all firewall rule sets. + - C(deleted) removes firewall rule sets. + - 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). + - Rule sets are identified by AFI and name. Deleting a rule set removes + all its rules. +""" + +EXAMPLES = r""" +- name: Merge firewall rules + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + source: + address: 192.168.1.0/24 + destination: + port: "80" + - number: 20 + action: drop + state: invalid + - afi: ipv6 + rule_sets: + - name: RULE-SET6 + default_action: accept + rules: + - number: 10 + action: accept + state: merged + +- name: Delete all firewall rules + vyos.rest.vyos_firewall_rules: + state: deleted + +- name: Gather firewall rules + vyos.rest.vyos_firewall_rules: + state: gathered +""" + +RETURN = r""" +before: + description: Firewall rules configuration before this module ran. + returned: always + type: list +after: + description: Firewall rules 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 firewall rules 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 + + +_BASE = ["firewall"] +_AFIS = ["ipv4", "ipv6"] + + +def _parse_rule(rule_num, data): + rule = {"number": int(rule_num)} + data = data or {} + if "action" in data: + rule["action"] = data["action"] + if "description" in data: + rule["description"] = data["description"] + if "disable" in data: + rule["disable"] = True + if "protocol" in data: + rule["protocol"] = data["protocol"] + if "state" in data: + rule["state"] = data["state"] + if "log" in data: + rule["log"] = True + + src = data.get("source", {}) or {} + if src: + rule["source"] = {} + if "address" in src: + rule["source"]["address"] = src["address"] + if "group" in src: + grp = src["group"] + if isinstance(grp, dict): + rule["source"]["group"] = list(grp.values())[0] if grp else None + else: + rule["source"]["group"] = grp + if "port" in src: + rule["source"]["port"] = src["port"] + + dst = data.get("destination", {}) or {} + if dst: + rule["destination"] = {} + if "address" in dst: + rule["destination"]["address"] = dst["address"] + if "group" in dst: + grp = dst["group"] + if isinstance(grp, dict): + rule["destination"]["group"] = list(grp.values())[0] if grp else None + else: + rule["destination"]["group"] = grp + if "port" in dst: + rule["destination"]["port"] = dst["port"] + + icmp = data.get("icmp", {}) or {} + if icmp: + rule["icmp"] = {} + if "type" in icmp: + rule["icmp"]["type"] = int(icmp["type"]) + if "code" in icmp: + rule["icmp"]["code"] = int(icmp["code"]) + + return rule + + +def _parse_rule_set(rs_name, data): + rs = {"name": rs_name} + data = data or {} + if "default-action" in data: + rs["default_action"] = data["default-action"] + if "description" in data: + rs["description"] = data["description"] + rules_raw = data.get("rule", {}) or {} + if rules_raw and isinstance(rules_raw, dict): + rules = [ + _parse_rule(num, rdata) + for num, rdata in sorted( + rules_raw.items(), + key=lambda x: int(x[0]), + ) + ] + if rules: + rs["rules"] = rules + return rs + + +def get_running_config(vyos): + result = [] + for afi in _AFIS: + raw = vyos.get_config(_BASE + [afi, "name"]) + if not raw or not isinstance(raw, dict): + continue + # unwrap "name" key if present + raw = raw.get("name", raw) + if not raw or not isinstance(raw, dict): + continue + rule_sets = [_parse_rule_set(name, data) for name, data in sorted(raw.items())] + if rule_sets: + result.append({"afi": afi, "rule_sets": rule_sets}) + return result + + +def _rule_cmds(rs_name, afi, rule, have_rule): + cmds = [] + rbase = _BASE + [afi, "name", rs_name, "rule", str(rule["number"])] + have_rule = have_rule or {} + + if rule.get("action") and rule["action"] != have_rule.get("action"): + cmds.append(("set", rbase + ["action", rule["action"]])) + if rule.get("description") and rule["description"] != have_rule.get("description"): + cmds.append(("set", rbase + ["description", rule["description"]])) + if rule.get("disable") and not have_rule.get("disable"): + cmds.append(("set", rbase + ["disable"])) + if rule.get("protocol") and rule["protocol"] != have_rule.get("protocol"): + cmds.append(("set", rbase + ["protocol", rule["protocol"]])) + if rule.get("state") and rule["state"] != have_rule.get("state"): + cmds.append(("set", rbase + ["state", rule["state"]])) + if rule.get("log") and not have_rule.get("log"): + cmds.append(("set", rbase + ["log"])) + + for endpoint in ["source", "destination"]: + want_ep = rule.get(endpoint) or {} + have_ep = have_rule.get(endpoint) or {} + if want_ep.get("address") and want_ep["address"] != have_ep.get("address"): + cmds.append(("set", rbase + [endpoint, "address", want_ep["address"]])) + if want_ep.get("port") and want_ep["port"] != have_ep.get("port"): + cmds.append(("set", rbase + [endpoint, "port", str(want_ep["port"])])) + if want_ep.get("group") and want_ep["group"] != have_ep.get("group"): + cmds.append( + ( + "set", + rbase + + [ + endpoint, + "group", + "address-group", + want_ep["group"], + ], + ), + ) + + icmp = rule.get("icmp") or {} + have_icmp = have_rule.get("icmp") or {} + if icmp.get("type") and icmp["type"] != have_icmp.get("type"): + cmds.append(("set", rbase + ["icmp", "type", str(icmp["type"])])) + if icmp.get("code") and icmp["code"] != have_icmp.get("code"): + cmds.append(("set", rbase + ["icmp", "code", str(icmp["code"])])) + + return cmds + + +def _rule_set_cmds(afi, rs, have_rs, state): + cmds = [] + rs_name = rs["name"] + rsbase = _BASE + [afi, "name", rs_name] + have_rs = have_rs or {} + + if rs.get("default_action") and rs["default_action"] != have_rs.get("default_action"): + cmds.append(("set", rsbase + ["default-action", rs["default_action"]])) + if rs.get("description") and rs["description"] != have_rs.get("description"): + cmds.append(("set", rsbase + ["description", rs["description"]])) + + have_rules = {r["number"]: r for r in (have_rs.get("rules") or [])} + want_rules = {r["number"]: r for r in (rs.get("rules") or [])} + + if state == "replaced": + for num in set(have_rules) - set(want_rules): + cmds.append(("delete", rsbase + ["rule", str(num)])) + + for num, rule in want_rules.items(): + cmds += _rule_cmds(rs_name, afi, rule, have_rules.get(num)) + + return cmds + + +def build_commands(config, have_list, state): + cmds = [] + + if state == "deleted": + if not config: + if have_list: + cmds.append(("delete", _BASE)) + else: + have_map = { + (e["afi"], rs["name"]): rs for e in have_list for rs in e.get("rule_sets", []) + } + for entry in config: + afi = entry["afi"] + for rs in entry.get("rule_sets") or []: + if (afi, rs["name"]) in have_map: + cmds.append(("delete", _BASE + [afi, "name", rs["name"]])) + return cmds + + have_map = {e["afi"]: {rs["name"]: rs for rs in e.get("rule_sets", [])} for e in have_list} + + if state == "overridden": + want_keys = { + (e["afi"], rs["name"]) for e in (config or []) for rs in e.get("rule_sets", []) + } + for e in have_list: + for rs in e.get("rule_sets", []): + if (e["afi"], rs["name"]) not in want_keys: + cmds.append(("delete", _BASE + [e["afi"], "name", rs["name"]])) + + for entry in config or []: + afi = entry["afi"] + have_afi = have_map.get(afi, {}) + + for rs in entry.get("rule_sets") or []: + have_rs = have_afi.get(rs["name"]) + + if state == "replaced" and have_rs: + # delete and rebuild if different + want_cmds = _rule_set_cmds(afi, rs, {}, "merged") + have_cmds = _rule_set_cmds( + afi, + { + "name": rs["name"], + "default_action": have_rs.get("default_action"), + "rules": have_rs.get("rules", []), + }, + {}, + "merged", + ) + if want_cmds != have_cmds: + cmds.append(("delete", _BASE + [afi, "name", rs["name"]])) + have_rs = None + + cmds += _rule_set_cmds( + afi, + rs, + have_rs, + state if state not in ("replaced", "overridden") else "merged", + ) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="list", + elements="dict", + options=dict( + afi=dict(type="str", choices=["ipv4", "ipv6"], required=True), + rule_sets=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + default_action=dict( + type="str", + choices=["accept", "drop", "reject"], + ), + description=dict(type="str"), + rules=dict( + type="list", + elements="dict", + options=dict( + number=dict(type="int", required=True), + action=dict( + type="str", + choices=[ + "accept", + "drop", + "reject", + "return", + "queue", + "continue", + ], + ), + description=dict(type="str"), + disable=dict(type="bool"), + protocol=dict(type="str"), + state=dict( + type="str", + choices=[ + "established", + "invalid", + "new", + "related", + ], + ), + log=dict(type="bool"), + source=dict( + type="dict", + options=dict( + address=dict(type="str"), + group=dict(type="str"), + port=dict(type="str"), + ), + ), + destination=dict( + type="dict", + options=dict( + address=dict(type="str"), + group=dict(type="str"), + port=dict(type="str"), + ), + ), + icmp=dict( + type="dict", + options=dict( + type=dict(type="int"), + code=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_user.py b/plugins/modules/vyos_user.py new file mode 100644 index 0000000..8ccb148 --- /dev/null +++ b/plugins/modules/vyos_user.py @@ -0,0 +1,302 @@ +#!/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_user +short_description: Manage user accounts on VyOS devices using REST API +description: + - Manages local user accounts on VyOS devices via the REST API. + - Uses REST API (C(connection=httpapi)) instead of CLI. + - Passwords are write-only. Once set, they cannot be read back in plaintext. + - Use C(update_password=on_create) to avoid resetting passwords on every run. +version_added: "1.0.0" +author: + - VyOS Community (@vyos) +options: + users: + description: List of user definitions. + type: list + elements: dict + suboptions: + name: + description: Username. + type: str + required: true + full_name: + description: Full name of the user. + type: str + password: + description: Plaintext password. Write-only — hashed on device immediately. + type: str + update_password: + description: + - Control when password is updated. + - C(always) updates the password on every run (default). + - C(on_create) only sets the password when the user is first created. + type: str + choices: [always, on_create] + default: always + public_keys: + description: SSH public keys for the user. + type: list + elements: dict + suboptions: + name: + description: Key identifier/name. + type: str + required: true + key: + description: Base64-encoded public key. + type: str + required: true + type: + description: Key type. + type: str + choices: [ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, + ecdsa-sha2-nistp521, ssh-ed25519] + required: true + state: + description: + - C(present) ensures users exist with the specified configuration. + - C(absent) removes specified users. + - C(gathered) returns current user configuration as structured data. + type: str + choices: [present, absent, gathered] + default: present +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). + - The C(vyos) user cannot be deleted as it is required for API access. + - Passwords are hashed immediately by VyOS and cannot be read back. +""" + +EXAMPLES = r""" +- name: Create user + vyos.rest.vyos_user: + users: + - name: alice + full_name: Alice Smith + password: securepassword + update_password: on_create + state: present + +- name: Add SSH public key + vyos.rest.vyos_user: + users: + - name: alice + public_keys: + - name: alice-laptop + type: ssh-rsa + key: AAAAB3NzaC1yc2EAAAADAQABAAAB... + state: present + +- name: Delete user + vyos.rest.vyos_user: + users: + - name: alice + state: absent + +- name: Gather all users + vyos.rest.vyos_user: + state: gathered +""" + +RETURN = r""" +before: + description: User configuration before this module ran. + returned: always + type: list +after: + description: User 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 user 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 + + +_BASE = ["system", "login", "user"] + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return [] + raw = raw.get("user", raw) + result = [] + for username, data in sorted(raw.items()): + user = {"name": username} + data = data or {} + if data.get("full-name"): + user["full_name"] = data["full-name"] + auth = data.get("authentication", {}) or {} + if auth.get("encrypted-password"): + user["encrypted_password"] = auth["encrypted-password"] + pub_keys = auth.get("public-keys", {}) or {} + if pub_keys and isinstance(pub_keys, dict): + keys = [] + for key_name, key_data in sorted(pub_keys.items()): + key_data = key_data or {} + k = {"name": key_name} + if key_data.get("key"): + k["key"] = key_data["key"] + if key_data.get("type"): + k["type"] = key_data["type"] + keys.append(k) + if keys: + user["public_keys"] = keys + result.append(user) + return result + + +def build_commands(users, have_list, state): + cmds = [] + have_map = {u["name"]: u for u in have_list} + + if state == "absent": + for user in users: + name = user["name"] + if name in have_map: + cmds.append(("delete", _BASE + [name])) + return cmds + + # state == "present" + for user in users: + name = user["name"] + have = have_map.get(name, {}) + ubase = _BASE + [name] + is_new = name not in have_map + + # full_name + if user.get("full_name") and user["full_name"] != have.get("full_name"): + cmds.append(("set", ubase + ["full-name", user["full_name"]])) + + # password + if user.get("password"): + update_pw = user.get("update_password", "always") + if update_pw == "always" or is_new: + cmds.append( + ( + "set", + ubase + + [ + "authentication", + "plaintext-password", + user["password"], + ], + ), + ) + + # public_keys + want_keys = {k["name"]: k for k in (user.get("public_keys") or [])} + have_keys = {k["name"]: k for k in (have.get("public_keys") or [])} + for key_name, key_data in want_keys.items(): + have_key = have_keys.get(key_name, {}) + kbase = ubase + ["authentication", "public-keys", key_name] + if key_data.get("key") and key_data["key"] != have_key.get("key"): + cmds.append(("set", kbase + ["key", key_data["key"]])) + if key_data.get("type") and key_data["type"] != have_key.get("type"): + cmds.append(("set", kbase + ["type", key_data["type"]])) + + return cmds + + +ARGUMENT_SPEC = dict( + users=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + full_name=dict(type="str"), + password=dict(type="str", no_log=True), + update_password=dict( + type="str", + choices=["always", "on_create"], + default="always", + ), + public_keys=dict( + type="list", + elements="dict", + options=dict( + name=dict(type="str", required=True), + key=dict(type="str", required=True, no_log=True), + type=dict( + type="str", + required=True, + choices=[ + "ssh-dss", + "ssh-rsa", + "ecdsa-sha2-nistp256", + "ecdsa-sha2-nistp384", + "ecdsa-sha2-nistp521", + "ssh-ed25519", + ], + ), + ), + ), + ), + ), + state=dict( + default="present", + choices=["present", "absent", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + users = module.params.get("users") or [] + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(users, 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/tests/integration/targets/vyos_bgp_address_family/aliases b/tests/integration/targets/vyos_bgp_address_family/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_bgp_address_family/defaults/main.yaml b/tests/integration/targets/vyos_bgp_address_family/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_bgp_address_family/tasks/httpapi.yaml b/tests/integration/targets/vyos_bgp_address_family/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/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_bgp_address_family/tasks/main.yaml b/tests/integration/targets/vyos_bgp_address_family/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/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_bgp_address_family/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..e4487d9 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_populate_config.yaml @@ -0,0 +1,32 @@ +--- +- name: Setup BGP global + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: merged + ignore_errors: true + +- name: Populate BGP address-family config + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + redistribute: + - protocol: connected + metric: 10 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + nexthop_self: true + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..5272cb4 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove BGP configuration + vyos.rest.vyos_bgp_global: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..ce1ab70 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/deleted.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_bgp_address_family deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete BGP address-family configuration + register: result + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete BGP address-family configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..e686a32 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_bgp_address_family gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather BGP address-family configuration + register: result + vyos.rest.vyos_bgp_address_family: + state: gathered + + - assert: + that: + - result.gathered.as_number == 65000 + - result.gathered.address_family | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - result.gathered.neighbors | selectattr('neighbor_address', 'eq', '192.0.2.1') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/merged.yaml new file mode 100644 index 0000000..69edb8c --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/merged.yaml @@ -0,0 +1,65 @@ +--- +- debug: + msg: START vyos_bgp_address_family merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Setup BGP global + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: merged + + - name: Merge BGP address-family configuration + register: result + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + redistribute: + - protocol: connected + metric: 10 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge BGP address-family configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + redistribute: + - protocol: connected + metric: 10 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..9a497d2 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/overridden.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_bgp_address_family overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override BGP address-family configuration + register: result + vyos.rest.vyos_bgp_address_family: &id001 + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.9.0/24 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override BGP address-family configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_address_family: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..133ae39 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/replaced.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_bgp_address_family replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace BGP address-family configuration + register: result + vyos.rest.vyos_bgp_address_family: &id001 + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.3.0/24 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace BGP address-family configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_address_family: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..cea4611 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/tests/httpapi/rtt.yaml @@ -0,0 +1,62 @@ +--- +- debug: + msg: START vyos_bgp_address_family round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Setup BGP global + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: merged + + - name: RTT - Apply base configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.2.0/24 + neighbors: + - neighbor_address: 192.0.2.1 + address_family: + - afi: ipv4 + soft_reconfiguration: true + state: merged + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_bgp_address_family: + state: gathered + + - assert: + that: + - gathered.gathered.address_family | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - gathered.gathered.neighbors | selectattr('neighbor_address', 'eq', '192.0.2.1') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_bgp_address_family: + config: + as_number: 65000 + address_family: + - afi: ipv4 + networks: + - prefix: 192.0.3.0/24 + state: replaced + + - name: RTT - Gather after modify + register: gathered2 + vyos.rest.vyos_bgp_address_family: + state: gathered + + - assert: + that: + - (gathered2.gathered.address_family | selectattr('afi', 'eq', 'ipv4') | first).networks[0].prefix == '192.0.3.0/24' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_address_family/vars/main.yaml b/tests/integration/targets/vyos_bgp_address_family/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_address_family/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_bgp_global/aliases b/tests/integration/targets/vyos_bgp_global/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_bgp_global/defaults/main.yaml b/tests/integration/targets/vyos_bgp_global/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_bgp_global/tasks/httpapi.yaml b/tests/integration/targets/vyos_bgp_global/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/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_bgp_global/tasks/main.yaml b/tests/integration/targets/vyos_bgp_global/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/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_bgp_global/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..c8eb061 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/_populate_config.yaml @@ -0,0 +1,19 @@ +--- +- name: Populate BGP global config for testing + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + description: peer1 + timers: + holdtime: 30 + keepalive: 10 + peer_groups: + - peer_group: PG1 + remote_as: 65002 + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..5272cb4 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove BGP configuration + vyos.rest.vyos_bgp_global: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..ed2c285 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_bgp_global deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete BGP configuration + register: result + vyos.rest.vyos_bgp_global: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete BGP configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_global: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..b38dba0 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/gathered.yaml @@ -0,0 +1,22 @@ +--- +- debug: + msg: START vyos_bgp_global gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather BGP global configuration + register: result + vyos.rest.vyos_bgp_global: + state: gathered + + - assert: + that: + - result.gathered.as_number == 65000 + - result.gathered.parameters.router_id == "192.0.1.1" + - result.gathered.neighbors | selectattr('neighbor_address', 'eq', '192.0.2.1') | list | length == 1 + - result.gathered.peer_groups | selectattr('peer_group', 'eq', 'PG1') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/merged.yaml new file mode 100644 index 0000000..10ca4da --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/merged.yaml @@ -0,0 +1,44 @@ +--- +- debug: + msg: START vyos_bgp_global merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge BGP global configuration + register: result + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + description: peer1 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge BGP global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + description: peer1 + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..603aff3 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/overridden.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_bgp_global overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override BGP global configuration + register: result + vyos.rest.vyos_bgp_global: &id001 + config: + as_number: 65000 + parameters: + router_id: 192.0.1.2 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override BGP global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..1a7d615 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/replaced.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_bgp_global replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace BGP global configuration + register: result + vyos.rest.vyos_bgp_global: &id001 + config: + as_number: 65000 + parameters: + router_id: 192.0.1.2 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace BGP global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_bgp_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_bgp_global/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..490ec1a --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/tests/httpapi/rtt.yaml @@ -0,0 +1,50 @@ +--- +- debug: + msg: START vyos_bgp_global round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.1 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: merged + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_bgp_global: + state: gathered + + - assert: + that: + - gathered.gathered.as_number == 65000 + - gathered.gathered.parameters.router_id == "192.0.1.1" + + - name: RTT - Modify configuration + vyos.rest.vyos_bgp_global: + config: + as_number: 65000 + parameters: + router_id: 192.0.1.2 + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65001 + state: replaced + + - name: RTT - Gather after modify + register: gathered2 + vyos.rest.vyos_bgp_global: + state: gathered + + - assert: + that: + - gathered2.gathered.parameters.router_id == "192.0.1.2" + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_bgp_global/vars/main.yaml b/tests/integration/targets/vyos_bgp_global/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_bgp_global/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_facts/aliases b/tests/integration/targets/vyos_facts/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_facts/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_facts/defaults/main.yaml b/tests/integration/targets/vyos_facts/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_facts/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_facts/tasks/httpapi.yaml b/tests/integration/targets/vyos_facts/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_facts/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_facts/tasks/main.yaml b/tests/integration/targets/vyos_facts/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_facts/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_facts/tests/httpapi/all.yaml b/tests/integration/targets/vyos_facts/tests/httpapi/all.yaml new file mode 100644 index 0000000..419634f --- /dev/null +++ b/tests/integration/targets/vyos_facts/tests/httpapi/all.yaml @@ -0,0 +1,16 @@ +--- +- debug: + msg: START vyos_facts all integration tests on connection={{ ansible_connection }} + +- block: + - name: Gather all facts + vyos.rest.vyos_facts: + gather_subset: all + + - assert: + that: + - ansible_facts.vyos_hostname is defined + - ansible_facts.vyos_interfaces is defined + - ansible_facts.vyos_users is defined + - ansible_facts.vyos_config is defined + - ansible_facts.vyos_logging is defined diff --git a/tests/integration/targets/vyos_facts/tests/httpapi/default.yaml b/tests/integration/targets/vyos_facts/tests/httpapi/default.yaml new file mode 100644 index 0000000..ff1caee --- /dev/null +++ b/tests/integration/targets/vyos_facts/tests/httpapi/default.yaml @@ -0,0 +1,13 @@ +--- +- debug: + msg: START vyos_facts default integration tests on connection={{ ansible_connection }} + +- block: + - name: Gather default facts + vyos.rest.vyos_facts: + + - assert: + that: + - ansible_facts.vyos_hostname is defined + - ansible_facts.vyos_interfaces is defined + - ansible_facts.vyos_hostname | length > 0 diff --git a/tests/integration/targets/vyos_facts/tests/httpapi/subsets.yaml b/tests/integration/targets/vyos_facts/tests/httpapi/subsets.yaml new file mode 100644 index 0000000..eb4ca4f --- /dev/null +++ b/tests/integration/targets/vyos_facts/tests/httpapi/subsets.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_facts subsets integration tests on connection={{ ansible_connection }} + +- block: + - name: Gather hostname only + vyos.rest.vyos_facts: + gather_subset: + - hostname + + - assert: + that: + - ansible_facts.vyos_hostname is defined + + - name: Gather users + vyos.rest.vyos_facts: + gather_subset: + - users + + - assert: + that: + - ansible_facts.vyos_users is defined + - ansible_facts.vyos_users | selectattr('name', 'eq', 'vyos') | list | length == 1 + + - name: Gather config + vyos.rest.vyos_facts: + gather_subset: + - config + + - assert: + that: + - ansible_facts.vyos_config is defined + - ansible_facts.vyos_config.system is defined diff --git a/tests/integration/targets/vyos_facts/vars/main.yaml b/tests/integration/targets/vyos_facts/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_facts/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_firewall_global/aliases b/tests/integration/targets/vyos_firewall_global/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_firewall_global/defaults/main.yaml b/tests/integration/targets/vyos_firewall_global/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_firewall_global/tasks/httpapi.yaml b/tests/integration/targets/vyos_firewall_global/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/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_firewall_global/tasks/main.yaml b/tests/integration/targets/vyos_firewall_global/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/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_firewall_global/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..c891369 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/_populate_config.yaml @@ -0,0 +1,22 @@ +--- +- name: Populate firewall global config for testing + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + description: Web servers + address: + - 192.168.1.10 + - 192.168.1.11 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + port_group: + - name: WEB-PORTS + port: + - "80" + - "443" + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..eb0c596 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove firewall global configuration + vyos.rest.vyos_firewall_global: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..ade5fe4 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_firewall_global deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete firewall global configuration + register: result + vyos.rest.vyos_firewall_global: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete firewall global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_global: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..6f470bd --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_firewall_global gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather firewall global configuration + register: result + vyos.rest.vyos_firewall_global: + state: gathered + + - assert: + that: + - result.gathered.group.address_group | selectattr('name', 'eq', 'SERVERS') | list | length == 1 + - result.gathered.group.network_group | selectattr('name', 'eq', 'LAN') | list | length == 1 + - result.gathered.group.port_group | selectattr('name', 'eq', 'WEB-PORTS') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/merged.yaml new file mode 100644 index 0000000..aaaa67a --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/merged.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_firewall_global merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge firewall global configuration + register: result + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + address: + - 192.168.1.10 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge firewall global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + address: + - 192.168.1.10 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..04d7c95 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/overridden.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_firewall_global overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override firewall global configuration + register: result + vyos.rest.vyos_firewall_global: &id001 + config: + group: + network_group: + - name: DMZ + network: + - 10.0.0.0/8 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override firewall global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..b5af06f --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/replaced.yaml @@ -0,0 +1,34 @@ +--- +- debug: + msg: START vyos_firewall_global replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace firewall global configuration + register: result + vyos.rest.vyos_firewall_global: &id001 + config: + group: + network_group: + - name: DMZ + network: + - 10.0.0.0/8 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace firewall global configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_global: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_firewall_global/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..56ce94d --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/tests/httpapi/rtt.yaml @@ -0,0 +1,55 @@ +--- +- debug: + msg: START vyos_firewall_global round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_firewall_global: + config: + group: + address_group: + - name: SERVERS + address: + - 192.168.1.10 + network_group: + - name: LAN + network: + - 192.168.0.0/16 + state: merged + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_firewall_global: + state: gathered + + - assert: + that: + - gathered.gathered.group.address_group | selectattr('name', 'eq', 'SERVERS') | list | length == 1 + - gathered.gathered.group.network_group | selectattr('name', 'eq', 'LAN') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_firewall_global: + config: + group: + network_group: + - name: LAN + network: + - 192.168.0.0/16 + - name: DMZ + network: + - 10.0.0.0/8 + state: merged + + - name: RTT - Gather after modify + register: gathered2 + vyos.rest.vyos_firewall_global: + state: gathered + + - assert: + that: + - gathered2.gathered.group.network_group | selectattr('name', 'eq', 'DMZ') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_global/vars/main.yaml b/tests/integration/targets/vyos_firewall_global/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_global/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_firewall_interfaces/aliases b/tests/integration/targets/vyos_firewall_interfaces/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_firewall_interfaces/defaults/main.yaml b/tests/integration/targets/vyos_firewall_interfaces/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_firewall_interfaces/tasks/httpapi.yaml b/tests/integration/targets/vyos_firewall_interfaces/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_firewall_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_firewall_interfaces/tasks/main.yaml b/tests/integration/targets/vyos_firewall_interfaces/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_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_firewall_interfaces/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..d2dddc7 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_populate_config.yaml @@ -0,0 +1,23 @@ +--- +- name: Populate firewall interfaces config for testing + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + - number: 20 + action: drop + state: invalid + - hook: forward + default_action: accept + - afi: ipv6 + hooks: + - hook: input + default_action: accept + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..14b0216 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove firewall interfaces configuration + vyos.rest.vyos_firewall_interfaces: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..00025f7 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_firewall_interfaces deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all firewall interfaces configuration + register: result + vyos.rest.vyos_firewall_interfaces: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete all firewall interfaces configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_interfaces: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..27ad537 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_firewall_interfaces gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather firewall interfaces configuration + register: result + vyos.rest.vyos_firewall_interfaces: + state: gathered + + - assert: + that: + - result.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - result.gathered | selectattr('afi', 'eq', 'ipv6') | list | length == 1 + - (result.gathered | selectattr('afi', 'eq', 'ipv4') | first).hooks | selectattr('hook', 'eq', 'input') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/merged.yaml new file mode 100644 index 0000000..d89530d --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/merged.yaml @@ -0,0 +1,46 @@ +--- +- debug: + msg: START vyos_firewall_interfaces merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge firewall interfaces configuration + register: result + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge firewall interfaces configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..8a20a48 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/overridden.yaml @@ -0,0 +1,33 @@ +--- +- debug: + msg: START vyos_firewall_interfaces overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override firewall interfaces configuration + register: result + vyos.rest.vyos_firewall_interfaces: &id001 + config: + - afi: ipv4 + hooks: + - hook: forward + default_action: accept + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override firewall interfaces configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..f782fc6 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/replaced.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_firewall_interfaces replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace firewall interfaces configuration + register: result + vyos.rest.vyos_firewall_interfaces: &id001 + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace firewall interfaces configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_interfaces: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..229bc7f --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/rtt.yaml @@ -0,0 +1,53 @@ +--- +- debug: + msg: START vyos_firewall_interfaces round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + state: merged + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_firewall_interfaces: + state: gathered + + - assert: + that: + - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_firewall_interfaces: + config: + - afi: ipv4 + hooks: + - hook: input + default_action: accept + rules: + - number: 10 + action: accept + state: established + state: replaced + + - name: RTT - Gather after modify + register: gathered2 + vyos.rest.vyos_firewall_interfaces: + state: gathered + + - assert: + that: + - (gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | first).hooks[0].default_action == 'accept' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_interfaces/vars/main.yaml b/tests/integration/targets/vyos_firewall_interfaces/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_interfaces/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_firewall_rules/aliases b/tests/integration/targets/vyos_firewall_rules/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_firewall_rules/defaults/main.yaml b/tests/integration/targets/vyos_firewall_rules/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_firewall_rules/tasks/httpapi.yaml b/tests/integration/targets/vyos_firewall_rules/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/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_firewall_rules/tasks/main.yaml b/tests/integration/targets/vyos_firewall_rules/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/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_firewall_rules/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..7654625 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/_populate_config.yaml @@ -0,0 +1,28 @@ +--- +- name: Populate firewall rules config for testing + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + source: + address: 192.168.1.0/24 + destination: + port: "80" + - number: 20 + action: drop + state: invalid + - afi: ipv6 + rule_sets: + - name: RULE-SET6 + default_action: accept + rules: + - number: 10 + action: accept + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..52866df --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove firewall rules configuration + vyos.rest.vyos_firewall_rules: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..db4cbf0 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_firewall_rules deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all firewall rules configuration + register: result + vyos.rest.vyos_firewall_rules: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete all firewall rules configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_rules: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..6729976 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_firewall_rules gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather firewall rules configuration + register: result + vyos.rest.vyos_firewall_rules: + state: gathered + + - assert: + that: + - result.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + - result.gathered | selectattr('afi', 'eq', 'ipv6') | list | length == 1 + - (result.gathered | selectattr('afi', 'eq', 'ipv4') | first).rule_sets | selectattr('name', 'eq', 'RULE-SET1') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/merged.yaml new file mode 100644 index 0000000..4dc02ca --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/merged.yaml @@ -0,0 +1,50 @@ +--- +- debug: + msg: START vyos_firewall_rules merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge firewall rules configuration + register: result + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + source: + address: 192.168.1.0/24 + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge firewall rules configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + source: + address: 192.168.1.0/24 + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..d5db4b9 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/overridden.yaml @@ -0,0 +1,36 @@ +--- +- debug: + msg: START vyos_firewall_rules overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override firewall rules configuration + register: result + vyos.rest.vyos_firewall_rules: &id001 + config: + - afi: ipv4 + rule_sets: + - name: NEW-SET + default_action: accept + rules: + - number: 10 + action: accept + state: overridden + + - assert: + that: + - result.changed == true + + - name: Override firewall rules configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_rules: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..92033c4 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/replaced.yaml @@ -0,0 +1,36 @@ +--- +- debug: + msg: START vyos_firewall_rules replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace firewall rules configuration + register: result + vyos.rest.vyos_firewall_rules: &id001 + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: accept + rules: + - number: 10 + action: accept + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace firewall rules configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_firewall_rules: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..193f03e --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/tests/httpapi/rtt.yaml @@ -0,0 +1,52 @@ +--- +- debug: + msg: START vyos_firewall_rules round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: drop + rules: + - number: 10 + action: accept + protocol: tcp + state: merged + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_firewall_rules: + state: gathered + + - assert: + that: + - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_firewall_rules: + config: + - afi: ipv4 + rule_sets: + - name: RULE-SET1 + default_action: accept + rules: + - number: 10 + action: accept + state: replaced + + - name: RTT - Gather after modify + register: gathered2 + vyos.rest.vyos_firewall_rules: + state: gathered + + - assert: + that: + - (gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | first).rule_sets[0].default_action == 'accept' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_firewall_rules/vars/main.yaml b/tests/integration/targets/vyos_firewall_rules/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_firewall_rules/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/integration/targets/vyos_user/aliases b/tests/integration/targets/vyos_user/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_user/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_user/defaults/main.yaml b/tests/integration/targets/vyos_user/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_user/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_user/tasks/httpapi.yaml b/tests/integration/targets/vyos_user/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_user/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_user/tasks/main.yaml b/tests/integration/targets/vyos_user/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_user/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_user/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_user/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..54cf0f9 --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/httpapi/_remove_config.yaml @@ -0,0 +1,8 @@ +--- +- name: Remove test users + vyos.rest.vyos_user: + users: + - name: testuser + - name: testuser2 + state: absent + ignore_errors: true diff --git a/tests/integration/targets/vyos_user/tests/httpapi/absent.yaml b/tests/integration/targets/vyos_user/tests/httpapi/absent.yaml new file mode 100644 index 0000000..c89da2e --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/httpapi/absent.yaml @@ -0,0 +1,43 @@ +--- +- debug: + msg: START vyos_user absent integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Create users for deletion test + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User + password: S3cur3P@ss! + - name: testuser2 + full_name: Test User 2 + password: S3cur3P@ss! + state: present + + - name: Delete specific user + register: result + vyos.rest.vyos_user: + users: + - name: testuser + state: absent + + - assert: + that: + - result.changed == true + + - name: Delete specific user (IDEMPOTENT) + register: result + vyos.rest.vyos_user: + users: + - name: testuser + state: absent + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_user/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_user/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..0f651e7 --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/httpapi/gathered.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_user gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Create user for gathering + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User + password: S3cur3P@ss! + state: present + + - name: Gather users + register: result + vyos.rest.vyos_user: + state: gathered + + - assert: + that: + - result.gathered | selectattr('name', 'eq', 'testuser') | list | length == 1 + - result.gathered | selectattr('name', 'eq', 'vyos') | list | length == 1 + - (result.gathered | selectattr('name', 'eq', 'testuser') | first).full_name == 'Test User' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_user/tests/httpapi/present.yaml b/tests/integration/targets/vyos_user/tests/httpapi/present.yaml new file mode 100644 index 0000000..1797772 --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/httpapi/present.yaml @@ -0,0 +1,63 @@ +--- +- debug: + msg: START vyos_user present integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Create user + register: result + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User + password: S3cur3P@ss! + update_password: on_create + state: present + + - assert: + that: + - result.changed == true + + - name: Create user (IDEMPOTENT) + register: result + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User + password: S3cur3P@ss! + update_password: on_create + state: present + + - assert: + that: + - result.changed == false + - result.commands == [] + + - name: Update full_name + register: result + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User Updated + state: present + + - assert: + that: + - result.changed == true + + - name: Update full_name (IDEMPOTENT) + register: result + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User Updated + state: present + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_user/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_user/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..6f35676 --- /dev/null +++ b/tests/integration/targets/vyos_user/tests/httpapi/rtt.yaml @@ -0,0 +1,45 @@ +--- +- debug: + msg: START vyos_user round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Create user + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User + password: S3cur3P@ss! + update_password: on_create + state: present + + - name: RTT - Gather + register: gathered + vyos.rest.vyos_user: + state: gathered + + - name: RTT - Assert user exists + assert: + that: + - gathered.gathered | selectattr('name', 'eq', 'testuser') | list | length == 1 + + - name: RTT - Update full_name + vyos.rest.vyos_user: + users: + - name: testuser + full_name: Test User Modified + state: present + + - name: RTT - Gather after update + register: gathered2 + vyos.rest.vyos_user: + state: gathered + + - name: RTT - Assert update applied + assert: + that: + - (gathered2.gathered | selectattr('name', 'eq', 'testuser') | first).full_name == 'Test User Modified' + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_user/vars/main.yaml b/tests/integration/targets/vyos_user/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_user/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/unit/fixtures/bgp_af_running.json b/tests/unit/fixtures/bgp_af_running.json new file mode 100644 index 0000000..f9c36d9 --- /dev/null +++ b/tests/unit/fixtures/bgp_af_running.json @@ -0,0 +1,29 @@ +{ + "system-as": "65000", + "address-family": { + "ipv4-unicast": { + "network": { + "192.0.2.0/24": {}, + "192.0.3.0/24": {} + }, + "redistribute": { + "connected": { "metric": "10" }, + "static": {} + } + } + }, + "neighbor": { + "192.0.2.1": { + "remote-as": "65001", + "address-family": { + "ipv4-unicast": { + "nexthop-self": {}, + "soft-reconfiguration": { "inbound": {} } + }, + "ipv6-unicast": { + "soft-reconfiguration": { "inbound": {} } + } + } + } + } +} diff --git a/tests/unit/fixtures/bgp_global_running.json b/tests/unit/fixtures/bgp_global_running.json new file mode 100644 index 0000000..dc76159 --- /dev/null +++ b/tests/unit/fixtures/bgp_global_running.json @@ -0,0 +1,27 @@ +{ + "system-as": "65000", + "parameters": { + "router-id": "192.0.1.1", + "log-neighbor-changes": {} + }, + "neighbor": { + "192.0.2.1": { + "remote-as": "65001", + "description": "peer1", + "timers": { + "holdtime": "30", + "keepalive": "10" + } + }, + "192.0.2.2": { + "remote-as": "65002", + "ebgp-multihop": "2", + "update-source": "eth0" + } + }, + "peer-group": { + "PG1": { + "remote-as": "65003" + } + } +} diff --git a/tests/unit/fixtures/facts_interfaces.json b/tests/unit/fixtures/facts_interfaces.json new file mode 100644 index 0000000..b58e17f --- /dev/null +++ b/tests/unit/fixtures/facts_interfaces.json @@ -0,0 +1,11 @@ +{ + "ethernet": { + "eth0": { "address": "dhcp", "hw-id": "52:54:00:65:5a:24" }, + "eth1": { + "description": "uplink", + "hw-id": "52:54:00:96:42:93", + "mtu": "1500" + }, + "eth2": { "hw-id": "52:54:00:75:31:fa" } + } +} diff --git a/tests/unit/fixtures/facts_system.json b/tests/unit/fixtures/facts_system.json new file mode 100644 index 0000000..68091ed --- /dev/null +++ b/tests/unit/fixtures/facts_system.json @@ -0,0 +1,28 @@ +{ + "host-name": "vyos-test", + "login": { + "user": { + "vyos": { + "authentication": { + "encrypted-password": "$6$abc123" + } + }, + "alice": { + "full-name": "Alice Smith", + "authentication": { + "encrypted-password": "$6$def456", + "public-keys": { + "alice-key": { + "key": "AAAAB3...", + "type": "ssh-rsa" + } + } + } + } + } + }, + "syslog": { + "local": {}, + "console": {} + } +} diff --git a/tests/unit/fixtures/firewall_global_running.json b/tests/unit/fixtures/firewall_global_running.json new file mode 100644 index 0000000..9463cd7 --- /dev/null +++ b/tests/unit/fixtures/firewall_global_running.json @@ -0,0 +1,34 @@ +{ + "address-group": { + "SERVERS": { + "description": "Web servers", + "address": ["192.168.1.10", "192.168.1.11"] + }, + "DNS": { + "address": "8.8.8.8" + } + }, + "network-group": { + "LAN": { + "network": "192.168.0.0/16" + }, + "DMZ": { + "network": ["10.0.0.0/8", "172.16.0.0/12"] + } + }, + "port-group": { + "WEB-PORTS": { + "port": ["80", "443"] + } + }, + "interface-group": { + "LAN-IFACES": { + "interface": "eth1" + } + }, + "ipv6-network-group": { + "IPV6-LAN": { + "network": "2001:db8::/32" + } + } +} diff --git a/tests/unit/fixtures/firewall_interfaces_running.json b/tests/unit/fixtures/firewall_interfaces_running.json new file mode 100644 index 0000000..b0ce2d5 --- /dev/null +++ b/tests/unit/fixtures/firewall_interfaces_running.json @@ -0,0 +1,30 @@ +{ + "ipv4": { + "input": { + "filter": { + "default-action": "accept", + "rule": { + "10": { "action": "accept", "state": "established" }, + "20": { "action": "drop", "state": "invalid" } + } + } + }, + "forward": { + "filter": { + "default-action": "accept" + } + }, + "output": { + "filter": { + "default-action": "accept" + } + } + }, + "ipv6": { + "input": { + "filter": { + "default-action": "accept" + } + } + } +} diff --git a/tests/unit/fixtures/firewall_rules_running.json b/tests/unit/fixtures/firewall_rules_running.json new file mode 100644 index 0000000..8fbd12c --- /dev/null +++ b/tests/unit/fixtures/firewall_rules_running.json @@ -0,0 +1,33 @@ +{ + "ipv4": { + "name": { + "RULE-SET1": { + "default-action": "drop", + "rule": { + "10": { + "action": "accept", + "protocol": "tcp", + "source": { "address": "192.168.1.0/24" }, + "destination": { "port": "80" } + }, + "20": { + "action": "drop", + "state": "invalid" + } + } + } + } + }, + "ipv6": { + "name": { + "RULE-SET6": { + "default-action": "accept", + "rule": { + "10": { + "action": "accept" + } + } + } + } + } +} diff --git a/tests/unit/fixtures/user_running.json b/tests/unit/fixtures/user_running.json new file mode 100644 index 0000000..4bc9c19 --- /dev/null +++ b/tests/unit/fixtures/user_running.json @@ -0,0 +1,22 @@ +{ + "user": { + "vyos": { + "authentication": { + "encrypted-password": "$6$abc123", + "plaintext-password": "" + } + }, + "alice": { + "authentication": { + "encrypted-password": "$6$def456", + "public-keys": { + "alice-laptop": { + "key": "AAAAB3NzaC1yc2EAAAA", + "type": "ssh-rsa" + } + } + }, + "full-name": "Alice Smith" + } + } +} diff --git a/tests/unit/modules/test_vyos_bgp_address_family.py b/tests/unit/modules/test_vyos_bgp_address_family.py new file mode 100644 index 0000000..fc080af --- /dev/null +++ b/tests/unit/modules/test_vyos_bgp_address_family.py @@ -0,0 +1,238 @@ +# -*- 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_bgp_address_family import ( + build_commands, + get_running_config, +) + + +_BASE = ["protocols", "bgp"] + + +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("bgp_af_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestVyOSBgpAFGetRunning(VyOSModuleTestCase): + + def test_parses_as_number(self): + result = get_running_config(self.mock_vyos) + self.assertEqual(result["as_number"], 65000) + + def test_parses_global_af_networks(self): + result = get_running_config(self.mock_vyos) + ipv4 = next(af for af in result["address_family"] if af["afi"] == "ipv4") + prefixes = [n["prefix"] for n in ipv4["networks"]] + self.assertIn("192.0.2.0/24", prefixes) + self.assertIn("192.0.3.0/24", prefixes) + + def test_parses_global_af_redistribute(self): + result = get_running_config(self.mock_vyos) + ipv4 = next(af for af in result["address_family"] if af["afi"] == "ipv4") + protos = [r["protocol"] for r in ipv4["redistribute"]] + self.assertIn("connected", protos) + connected = next(r for r in ipv4["redistribute"] if r["protocol"] == "connected") + self.assertEqual(connected["metric"], 10) + + def test_parses_neighbor_af(self): + result = get_running_config(self.mock_vyos) + nb = next(n for n in result["neighbors"] if n["neighbor_address"] == "192.0.2.1") + afis = [af["afi"] for af in nb["address_family"]] + self.assertIn("ipv4", afis) + self.assertIn("ipv6", afis) + ipv4 = next(af for af in nb["address_family"] if af["afi"] == "ipv4") + self.assertTrue(ipv4["nexthop_self"]) + self.assertTrue(ipv4["soft_reconfiguration"]) + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSBgpAFBuildCommands(unittest.TestCase): + + def _have(self): + return { + "as_number": 65000, + "address_family": [ + { + "afi": "ipv4", + "networks": [{"prefix": "192.0.2.0/24"}], + "redistribute": [{"protocol": "connected", "metric": 10}], + }, + ], + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "address_family": [ + {"afi": "ipv4", "soft_reconfiguration": True, "nexthop_self": True}, + ], + }, + ], + } + + def test_deleted_removes_global_af(self): + cmds = build_commands({"as_number": 65000}, self._have(), "deleted") + self.assertIn(("delete", _BASE + ["address-family"]), cmds) + + def test_deleted_removes_neighbor_af(self): + cmds = build_commands({"as_number": 65000}, self._have(), "deleted") + self.assertIn( + ("delete", _BASE + ["neighbor", "192.0.2.1", "address-family"]), + cmds, + ) + + def test_merged_network(self): + config = { + "as_number": 65000, + "address_family": [ + {"afi": "ipv4", "networks": [{"prefix": "192.0.5.0/24"}]}, + ], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["address-family", "ipv4-unicast", "network", "192.0.5.0/24"]), + cmds, + ) + + def test_merged_redistribute(self): + config = { + "as_number": 65000, + "address_family": [ + {"afi": "ipv4", "redistribute": [{"protocol": "connected", "metric": 10}]}, + ], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["address-family", "ipv4-unicast", "redistribute", "connected"]), + cmds, + ) + self.assertIn( + ( + "set", + _BASE + + [ + "address-family", + "ipv4-unicast", + "redistribute", + "connected", + "metric", + "10", + ], + ), + cmds, + ) + + def test_merged_neighbor_soft_reconfig(self): + config = { + "as_number": 65000, + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "address_family": [ + {"afi": "ipv4", "soft_reconfiguration": True}, + ], + }, + ], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ( + "set", + _BASE + + [ + "neighbor", + "192.0.2.1", + "address-family", + "ipv4-unicast", + "soft-reconfiguration", + "inbound", + ], + ), + cmds, + ) + + def test_merged_idempotent(self): + have = self._have() + config = { + "as_number": 65000, + "address_family": [ + { + "afi": "ipv4", + "networks": [{"prefix": "192.0.2.0/24"}], + "redistribute": [{"protocol": "connected", "metric": 10}], + }, + ], + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "address_family": [ + {"afi": "ipv4", "soft_reconfiguration": True, "nexthop_self": True}, + ], + }, + ], + } + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + have = self._have() + config = { + "as_number": 65000, + "address_family": [ + { + "afi": "ipv4", + "networks": [{"prefix": "192.0.2.0/24"}], + "redistribute": [{"protocol": "connected", "metric": 10}], + }, + ], + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "address_family": [ + {"afi": "ipv4", "soft_reconfiguration": True, "nexthop_self": True}, + ], + }, + ], + } + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + have = self._have() + config = { + "as_number": 65000, + "address_family": [ + {"afi": "ipv4", "networks": [{"prefix": "192.0.9.0/24"}]}, + ], + } + cmds = build_commands(config, have, "replaced") + self.assertIn(("delete", _BASE + ["address-family"]), cmds) + self.assertIn( + ("set", _BASE + ["address-family", "ipv4-unicast", "network", "192.0.9.0/24"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_bgp_global.py b/tests/unit/modules/test_vyos_bgp_global.py new file mode 100644 index 0000000..f91516b --- /dev/null +++ b/tests/unit/modules/test_vyos_bgp_global.py @@ -0,0 +1,188 @@ +# -*- 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_bgp_global import ( + build_commands, + get_running_config, +) + + +_BASE = ["protocols", "bgp"] + + +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("bgp_global_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestVyOSBgpGlobalGetRunning(VyOSModuleTestCase): + + def test_parses_as_number(self): + result = get_running_config(self.mock_vyos) + self.assertEqual(result["as_number"], 65000) + + def test_parses_parameters(self): + result = get_running_config(self.mock_vyos) + self.assertEqual(result["parameters"]["router_id"], "192.0.1.1") + self.assertTrue(result["parameters"]["log_neighbor_changes"]) + + def test_parses_neighbors(self): + result = get_running_config(self.mock_vyos) + nb_addrs = [n["neighbor_address"] for n in result["neighbors"]] + self.assertIn("192.0.2.1", nb_addrs) + self.assertIn("192.0.2.2", nb_addrs) + nb1 = next(n for n in result["neighbors"] if n["neighbor_address"] == "192.0.2.1") + self.assertEqual(nb1["remote_as"], 65001) + self.assertEqual(nb1["description"], "peer1") + self.assertEqual(nb1["timers"]["holdtime"], 30) + self.assertEqual(nb1["timers"]["keepalive"], 10) + nb2 = next(n for n in result["neighbors"] if n["neighbor_address"] == "192.0.2.2") + self.assertEqual(nb2["ebgp_multihop"], 2) + self.assertEqual(nb2["update_source"], "eth0") + + def test_parses_peer_groups(self): + result = get_running_config(self.mock_vyos) + self.assertEqual(len(result["peer_groups"]), 1) + self.assertEqual(result["peer_groups"][0]["peer_group"], "PG1") + self.assertEqual(result["peer_groups"][0]["remote_as"], 65003) + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSBgpGlobalBuildCommands(unittest.TestCase): + + def _have(self): + return { + "as_number": 65000, + "parameters": {"router_id": "192.0.1.1"}, + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "remote_as": 65001, + "description": "peer1", + }, + ], + "peer_groups": [{"peer_group": "PG1", "remote_as": 65003}], + } + + def test_deleted_with_have(self): + cmds = build_commands({}, self._have(), "deleted") + self.assertEqual(cmds, [("delete", _BASE)]) + + def test_deleted_without_have(self): + cmds = build_commands({}, {}, "deleted") + self.assertEqual(cmds, []) + + def test_merged_as_number(self): + config = {"as_number": 65000} + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["system-as", "65000"]), cmds) + + def test_merged_router_id(self): + config = {"as_number": 65000, "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_neighbor(self): + config = { + "as_number": 65000, + "neighbors": [ + {"neighbor_address": "192.0.2.1", "remote_as": 65001}, + ], + } + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["neighbor", "192.0.2.1", "remote-as", "65001"]), cmds) + + def test_merged_neighbor_timers(self): + config = { + "as_number": 65000, + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "remote_as": 65001, + "timers": {"holdtime": 30, "keepalive": 10}, + }, + ], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["neighbor", "192.0.2.1", "timers", "holdtime", "30"]), + cmds, + ) + self.assertIn( + ("set", _BASE + ["neighbor", "192.0.2.1", "timers", "keepalive", "10"]), + cmds, + ) + + def test_merged_idempotent(self): + have = self._have() + config = { + "as_number": 65000, + "parameters": {"router_id": "192.0.1.1"}, + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "remote_as": 65001, + "description": "peer1", + }, + ], + "peer_groups": [{"peer_group": "PG1", "remote_as": 65003}], + } + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_replaced_idempotent(self): + have = self._have() + config = { + "as_number": 65000, + "parameters": {"router_id": "192.0.1.1"}, + "neighbors": [ + { + "neighbor_address": "192.0.2.1", + "remote_as": 65001, + "description": "peer1", + }, + ], + "peer_groups": [{"peer_group": "PG1", "remote_as": 65003}], + } + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + have = self._have() + config = {"as_number": 65000, "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_peer_group(self): + config = { + "as_number": 65000, + "peer_groups": [{"peer_group": "PG1", "remote_as": 65003}], + } + cmds = build_commands(config, {}, "merged") + self.assertIn(("set", _BASE + ["peer-group", "PG1", "remote-as", "65003"]), cmds) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_facts.py b/tests/unit/modules/test_vyos_facts.py new file mode 100644 index 0000000..3fcf82e --- /dev/null +++ b/tests/unit/modules/test_vyos_facts.py @@ -0,0 +1,97 @@ +# -*- 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_facts import ( + gather_bgp, + gather_hostname, + gather_interfaces, + gather_logging, + gather_users, +) + + +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 TestVyOSFactsGather(unittest.TestCase): + + def setUp(self): + self.mock_vyos = MagicMock() + self.system_fixture = load_fixture("facts_system.json") + self.interfaces_fixture = load_fixture("facts_interfaces.json") + + def test_gather_hostname(self): + self.mock_vyos.get_config = MagicMock(return_value=self.system_fixture) + result = gather_hostname(self.mock_vyos) + self.assertEqual(result, "vyos-test") + + def test_gather_hostname_empty(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = gather_hostname(self.mock_vyos) + self.assertEqual(result, "") + + def test_gather_interfaces(self): + self.mock_vyos.get_config = MagicMock(return_value=self.interfaces_fixture) + result = gather_interfaces(self.mock_vyos) + self.assertIn("ethernet", result) + self.assertIn("eth0", result["ethernet"]) + self.assertIn("eth1", result["ethernet"]) + self.assertEqual(result["ethernet"]["eth1"]["description"], "uplink") + + def test_gather_users(self): + self.mock_vyos.get_config = MagicMock( + return_value=self.system_fixture["login"], + ) + result = gather_users(self.mock_vyos) + names = [u["name"] for u in result] + self.assertIn("vyos", names) + self.assertIn("alice", names) + alice = next(u for u in result if u["name"] == "alice") + self.assertEqual(alice["full_name"], "Alice Smith") + self.assertIn("alice-key", alice["public_keys"]) + + def test_gather_users_empty(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = gather_users(self.mock_vyos) + self.assertEqual(result, []) + + def test_gather_users_none(self): + self.mock_vyos.get_config = MagicMock(return_value=None) + result = gather_users(self.mock_vyos) + self.assertEqual(result, []) + + def test_gather_bgp(self): + data = {"system-as": "65000", "parameters": {"router-id": "192.0.1.1"}} + self.mock_vyos.get_config = MagicMock(return_value=data) + result = gather_bgp(self.mock_vyos) + self.assertEqual(result["system-as"], "65000") + self.assertEqual(result["parameters"]["router-id"], "192.0.1.1") + + def test_gather_bgp_empty(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = gather_bgp(self.mock_vyos) + self.assertEqual(result, {}) + + def test_gather_logging(self): + self.mock_vyos.get_config = MagicMock( + return_value=self.system_fixture["syslog"], + ) + result = gather_logging(self.mock_vyos) + self.assertIn("local", result) + self.assertIn("console", result) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_firewall_global.py b/tests/unit/modules/test_vyos_firewall_global.py new file mode 100644 index 0000000..a507c0c --- /dev/null +++ b/tests/unit/modules/test_vyos_firewall_global.py @@ -0,0 +1,190 @@ +# -*- 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_firewall_global import ( + build_commands, + get_running_config, +) + + +_BASE = ["firewall", "group"] + + +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("firewall_global_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestVyOSFirewallGlobalGetRunning(VyOSModuleTestCase): + + def test_parses_address_groups(self): + result = get_running_config(self.mock_vyos) + groups = result["group"]["address_group"] + names = [g["name"] for g in groups] + self.assertIn("SERVERS", names) + self.assertIn("DNS", names) + servers = next(g for g in groups if g["name"] == "SERVERS") + self.assertEqual(servers["description"], "Web servers") + self.assertIn("192.168.1.10", servers["address"]) + self.assertIn("192.168.1.11", servers["address"]) + + def test_parses_network_groups(self): + result = get_running_config(self.mock_vyos) + groups = result["group"]["network_group"] + dmz = next(g for g in groups if g["name"] == "DMZ") + self.assertIn("10.0.0.0/8", dmz["network"]) + self.assertIn("172.16.0.0/12", dmz["network"]) + + def test_parses_port_groups(self): + result = get_running_config(self.mock_vyos) + groups = result["group"]["port_group"] + web = next(g for g in groups if g["name"] == "WEB-PORTS") + self.assertIn("80", web["port"]) + self.assertIn("443", web["port"]) + + def test_parses_interface_groups(self): + result = get_running_config(self.mock_vyos) + groups = result["group"]["interface_group"] + lan = next(g for g in groups if g["name"] == "LAN-IFACES") + self.assertIn("eth1", lan["interface"]) + + def test_parses_ipv6_network_groups(self): + result = get_running_config(self.mock_vyos) + groups = result["group"]["ipv6_network_group"] + ipv6 = next(g for g in groups if g["name"] == "IPV6-LAN") + self.assertIn("2001:db8::/32", ipv6["network"]) + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSFirewallGlobalBuildCommands(unittest.TestCase): + + def _have(self): + return { + "group": { + "address_group": [ + {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]}, + ], + "network_group": [ + {"name": "LAN", "network": ["192.168.0.0/16"]}, + ], + }, + } + + def test_deleted_with_have(self): + cmds = build_commands({}, self._have(), "deleted") + self.assertEqual(cmds, [("delete", _BASE)]) + + def test_deleted_without_have(self): + cmds = build_commands({}, {}, "deleted") + self.assertEqual(cmds, []) + + def test_merged_address_group(self): + config = { + "group": { + "address_group": [ + {"name": "SERVERS", "address": ["192.168.1.10"]}, + ], + }, + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["address-group", "SERVERS", "address", "192.168.1.10"]), + cmds, + ) + + def test_merged_network_group(self): + config = { + "group": { + "network_group": [ + {"name": "LAN", "network": ["192.168.0.0/16"]}, + ], + }, + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["network-group", "LAN", "network", "192.168.0.0/16"]), + cmds, + ) + + def test_merged_port_group(self): + config = { + "group": { + "port_group": [ + {"name": "WEB", "port": ["80", "443"]}, + ], + }, + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", _BASE + ["port-group", "WEB", "port", "80"]), + cmds, + ) + + def test_merged_idempotent(self): + have = self._have() + config = { + "group": { + "address_group": [ + {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]}, + ], + "network_group": [ + {"name": "LAN", "network": ["192.168.0.0/16"]}, + ], + }, + } + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_replaced_removes_extra_group(self): + have = self._have() + config = { + "group": { + "network_group": [ + {"name": "DMZ", "network": ["10.0.0.0/8"]}, + ], + }, + } + cmds = build_commands(config, have, "replaced") + paths = [c[1] for c in cmds] + self.assertIn(_BASE + ["address-group", "SERVERS"], paths) + self.assertIn(_BASE + ["network-group", "LAN"], paths) + + def test_replaced_idempotent(self): + have = self._have() + config = { + "group": { + "address_group": [ + {"name": "SERVERS", "address": ["192.168.1.10", "192.168.1.11"]}, + ], + "network_group": [ + {"name": "LAN", "network": ["192.168.0.0/16"]}, + ], + }, + } + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_firewall_interfaces.py b/tests/unit/modules/test_vyos_firewall_interfaces.py new file mode 100644 index 0000000..66b3883 --- /dev/null +++ b/tests/unit/modules/test_vyos_firewall_interfaces.py @@ -0,0 +1,168 @@ +# -*- 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_firewall_interfaces import ( + build_commands, + get_running_config, +) + + +_BASE = ["firewall"] + + +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("firewall_interfaces_running.json") + + def _set_afi(self, afi): + data = self.fixture.get(afi, {}) + self.mock_vyos.get_config = MagicMock(return_value=data) + + +class TestVyOSFirewallInterfacesGetRunning(VyOSModuleTestCase): + + def test_parses_ipv4_hooks(self): + self._set_afi("ipv4") + result = get_running_config(self.mock_vyos) + ipv4 = next((e for e in result if e["afi"] == "ipv4"), None) + self.assertIsNotNone(ipv4) + hook_names = [h["hook"] for h in ipv4["hooks"]] + self.assertIn("input", hook_names) + self.assertIn("forward", hook_names) + self.assertIn("output", hook_names) + + def test_parses_input_rules(self): + self._set_afi("ipv4") + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + input_hook = next(h for h in ipv4["hooks"] if h["hook"] == "input") + self.assertEqual(input_hook["default_action"], "accept") + self.assertEqual(len(input_hook["rules"]), 2) + r10 = next(r for r in input_hook["rules"] if r["number"] == 10) + self.assertEqual(r10["action"], "accept") + self.assertEqual(r10["state"], "established") + + def test_parses_ipv6_hooks(self): + self._set_afi("ipv6") + result = get_running_config(self.mock_vyos) + ipv6 = next((e for e in result if e["afi"] == "ipv6"), None) + self.assertIsNotNone(ipv6) + self.assertEqual(ipv6["hooks"][0]["hook"], "input") + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSFirewallInterfacesBuildCommands(unittest.TestCase): + + def _have(self): + return [ + { + "afi": "ipv4", + "hooks": [ + { + "hook": "input", + "default_action": "accept", + "rules": [ + {"number": 10, "action": "accept", "state": "established"}, + {"number": 20, "action": "drop", "state": "invalid"}, + ], + }, + {"hook": "forward", "default_action": "accept"}, + ], + }, + ] + + def test_deleted_all(self): + cmds = build_commands([], self._have(), "deleted") + paths = [c[1] for c in cmds] + self.assertIn(_BASE + ["ipv4", "input", "filter"], paths) + self.assertIn(_BASE + ["ipv4", "forward", "filter"], paths) + + def test_deleted_specific(self): + config = [{"afi": "ipv4", "hooks": [{"hook": "input"}]}] + cmds = build_commands(config, self._have(), "deleted") + self.assertIn(("delete", _BASE + ["ipv4", "input", "filter"]), cmds) + paths = [c[1] for c in cmds] + self.assertNotIn(_BASE + ["ipv4", "forward", "filter"], paths) + + def test_merged_hook(self): + config = [ + { + "afi": "ipv4", + "hooks": [ + { + "hook": "input", + "default_action": "accept", + "rules": [{"number": 10, "action": "accept", "state": "established"}], + }, + ], + }, + ] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", _BASE + ["ipv4", "input", "filter", "default-action", "accept"]), + cmds, + ) + self.assertIn( + ("set", _BASE + ["ipv4", "input", "filter", "rule", "10", "action", "accept"]), + cmds, + ) + + def test_merged_idempotent(self): + have = self._have() + config = [ + { + "afi": "ipv4", + "hooks": [ + { + "hook": "input", + "default_action": "accept", + "rules": [ + {"number": 10, "action": "accept", "state": "established"}, + {"number": 20, "action": "drop", "state": "invalid"}, + ], + }, + {"hook": "forward", "default_action": "accept"}, + ], + }, + ] + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_overridden_removes_extra_hook(self): + have = self._have() + config = [ + { + "afi": "ipv4", + "hooks": [ + {"hook": "output", "default_action": "accept"}, + ], + }, + ] + cmds = build_commands(config, have, "overridden") + paths = [c[1] for c in cmds] + self.assertIn(_BASE + ["ipv4", "input", "filter"], paths) + self.assertIn(_BASE + ["ipv4", "forward", "filter"], paths) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_firewall_rules.py b/tests/unit/modules/test_vyos_firewall_rules.py new file mode 100644 index 0000000..4eaad9e --- /dev/null +++ b/tests/unit/modules/test_vyos_firewall_rules.py @@ -0,0 +1,203 @@ +# -*- 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_firewall_rules import ( + build_commands, + get_running_config, +) + + +_BASE = ["firewall"] + + +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("firewall_rules_running.json") + + def _set_afi(self, afi): + data = self.fixture.get(afi, {}) + self.mock_vyos.get_config = MagicMock(return_value=data) + + +class TestVyOSFirewallRulesGetRunning(VyOSModuleTestCase): + + def test_parses_ipv4_rule_sets(self): + self._set_afi("ipv4") + result = get_running_config(self.mock_vyos) + ipv4 = next((e for e in result if e["afi"] == "ipv4"), None) + self.assertIsNotNone(ipv4) + rs = next(rs for rs in ipv4["rule_sets"] if rs["name"] == "RULE-SET1") + self.assertEqual(rs["default_action"], "drop") + self.assertEqual(len(rs["rules"]), 2) + r10 = next(r for r in rs["rules"] if r["number"] == 10) + self.assertEqual(r10["action"], "accept") + self.assertEqual(r10["protocol"], "tcp") + self.assertEqual(r10["source"]["address"], "192.168.1.0/24") + self.assertEqual(r10["destination"]["port"], "80") + + def test_parses_rule_state(self): + self._set_afi("ipv4") + result = get_running_config(self.mock_vyos) + ipv4 = next(e for e in result if e["afi"] == "ipv4") + rs = ipv4["rule_sets"][0] + r20 = next(r for r in rs["rules"] if r["number"] == 20) + self.assertEqual(r20["state"], "invalid") + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSFirewallRulesBuildCommands(unittest.TestCase): + + def _have(self): + return [ + { + "afi": "ipv4", + "rule_sets": [ + { + "name": "RULE-SET1", + "default_action": "drop", + "rules": [ + {"number": 10, "action": "accept", "protocol": "tcp"}, + {"number": 20, "action": "drop", "state": "invalid"}, + ], + }, + ], + }, + ] + + def test_deleted_all(self): + cmds = build_commands([], self._have(), "deleted") + self.assertIn(("delete", _BASE), cmds) + + def test_deleted_specific(self): + config = [{"afi": "ipv4", "rule_sets": [{"name": "RULE-SET1"}]}] + cmds = build_commands(config, self._have(), "deleted") + self.assertIn(("delete", _BASE + ["ipv4", "name", "RULE-SET1"]), cmds) + + def test_merged_rule_set(self): + config = [ + { + "afi": "ipv4", + "rule_sets": [ + { + "name": "NEW-SET", + "default_action": "accept", + "rules": [{"number": 10, "action": "accept"}], + }, + ], + }, + ] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", _BASE + ["ipv4", "name", "NEW-SET", "default-action", "accept"]), + cmds, + ) + self.assertIn( + ("set", _BASE + ["ipv4", "name", "NEW-SET", "rule", "10", "action", "accept"]), + cmds, + ) + + def test_merged_rule_with_protocol_and_source(self): + config = [ + { + "afi": "ipv4", + "rule_sets": [ + { + "name": "RULE-SET1", + "rules": [ + { + "number": 10, + "action": "accept", + "protocol": "tcp", + "source": {"address": "10.0.0.0/8"}, + }, + ], + }, + ], + }, + ] + cmds = build_commands(config, [], "merged") + self.assertIn( + ("set", _BASE + ["ipv4", "name", "RULE-SET1", "rule", "10", "protocol", "tcp"]), + cmds, + ) + self.assertIn( + ( + "set", + _BASE + + [ + "ipv4", + "name", + "RULE-SET1", + "rule", + "10", + "source", + "address", + "10.0.0.0/8", + ], + ), + cmds, + ) + + def test_merged_idempotent(self): + have = self._have() + config = [ + { + "afi": "ipv4", + "rule_sets": [ + { + "name": "RULE-SET1", + "default_action": "drop", + "rules": [ + {"number": 10, "action": "accept", "protocol": "tcp"}, + {"number": 20, "action": "drop", "state": "invalid"}, + ], + }, + ], + }, + ] + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_overridden_removes_extra_rule_set(self): + have = self._have() + config = [ + { + "afi": "ipv4", + "rule_sets": [ + { + "name": "NEW-SET", + "default_action": "accept", + "rules": [{"number": 10, "action": "accept"}], + }, + ], + }, + ] + cmds = build_commands(config, have, "overridden") + self.assertIn( + ("delete", _BASE + ["ipv4", "name", "RULE-SET1"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/unit/modules/test_vyos_user.py b/tests/unit/modules/test_vyos_user.py new file mode 100644 index 0000000..511ecef --- /dev/null +++ b/tests/unit/modules/test_vyos_user.py @@ -0,0 +1,172 @@ +# -*- 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_user import ( + build_commands, + get_running_config, +) + + +_BASE = ["system", "login", "user"] + + +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("user_running.json") + self.mock_vyos.get_config = MagicMock(return_value=self.fixture) + + +class TestVyOSUserGetRunning(VyOSModuleTestCase): + + def test_parses_users(self): + result = get_running_config(self.mock_vyos) + names = [u["name"] for u in result] + self.assertIn("vyos", names) + self.assertIn("alice", names) + + def test_parses_full_name(self): + result = get_running_config(self.mock_vyos) + alice = next(u for u in result if u["name"] == "alice") + self.assertEqual(alice["full_name"], "Alice Smith") + + def test_parses_encrypted_password(self): + result = get_running_config(self.mock_vyos) + alice = next(u for u in result if u["name"] == "alice") + self.assertEqual(alice["encrypted_password"], "$6$def456") + + def test_parses_public_keys(self): + result = get_running_config(self.mock_vyos) + alice = next(u for u in result if u["name"] == "alice") + self.assertEqual(len(alice["public_keys"]), 1) + key = alice["public_keys"][0] + self.assertEqual(key["name"], "alice-laptop") + self.assertEqual(key["type"], "ssh-rsa") + self.assertEqual(key["key"], "AAAAB3NzaC1yc2EAAAA") + + def test_empty_config(self): + self.mock_vyos.get_config = MagicMock(return_value={}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, []) + + +class TestVyOSUserBuildCommands(unittest.TestCase): + + def _have(self): + return [ + {"name": "vyos", "encrypted_password": "$6$abc123"}, + { + "name": "alice", + "full_name": "Alice Smith", + "encrypted_password": "$6$def456", + }, + ] + + def test_present_new_user_with_password(self): + users = [ + { + "name": "bob", + "full_name": "Bob Jones", + "password": "secret", + "update_password": "always", + }, + ] + cmds = build_commands(users, self._have(), "present") + self.assertIn(("set", _BASE + ["bob", "full-name", "Bob Jones"]), cmds) + self.assertIn( + ("set", _BASE + ["bob", "authentication", "plaintext-password", "secret"]), + cmds, + ) + + def test_present_update_password_always(self): + users = [{"name": "alice", "password": "newpass", "update_password": "always"}] + cmds = build_commands(users, self._have(), "present") + self.assertIn( + ("set", _BASE + ["alice", "authentication", "plaintext-password", "newpass"]), + cmds, + ) + + def test_present_update_password_on_create_existing(self): + users = [{"name": "alice", "password": "newpass", "update_password": "on_create"}] + cmds = build_commands(users, self._have(), "present") + paths = [c[1] for c in cmds] + self.assertNotIn( + _BASE + ["alice", "authentication", "plaintext-password", "newpass"], + paths, + ) + + def test_present_update_password_on_create_new(self): + users = [{"name": "bob", "password": "secret", "update_password": "on_create"}] + cmds = build_commands(users, self._have(), "present") + self.assertIn( + ("set", _BASE + ["bob", "authentication", "plaintext-password", "secret"]), + cmds, + ) + + def test_present_idempotent_full_name(self): + users = [{"name": "alice", "full_name": "Alice Smith"}] + cmds = build_commands(users, self._have(), "present") + self.assertEqual(cmds, []) + + def test_present_update_full_name(self): + users = [{"name": "alice", "full_name": "Alice Updated"}] + cmds = build_commands(users, self._have(), "present") + self.assertIn( + ("set", _BASE + ["alice", "full-name", "Alice Updated"]), + cmds, + ) + + def test_absent_existing_user(self): + users = [{"name": "alice"}] + cmds = build_commands(users, self._have(), "absent") + self.assertIn(("delete", _BASE + ["alice"]), cmds) + + def test_absent_nonexistent_user(self): + users = [{"name": "bob"}] + cmds = build_commands(users, self._have(), "absent") + self.assertEqual(cmds, []) + + def test_present_public_key(self): + users = [ + { + "name": "alice", + "public_keys": [ + {"name": "new-key", "key": "AAAAB3...", "type": "ssh-ed25519"}, + ], + }, + ] + cmds = build_commands(users, self._have(), "present") + self.assertIn( + ( + "set", + _BASE + ["alice", "authentication", "public-keys", "new-key", "key", "AAAAB3..."], + ), + cmds, + ) + self.assertIn( + ( + "set", + _BASE + + ["alice", "authentication", "public-keys", "new-key", "type", "ssh-ed25519"], + ), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() |
