summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--docs/vyos.rest.vyos_bgp_address_family_module.rst1024
-rw-r--r--docs/vyos.rest.vyos_bgp_global_module.rst876
-rw-r--r--docs/vyos.rest.vyos_facts_module.rst296
-rw-r--r--docs/vyos.rest.vyos_firewall_global_module.rst642
-rw-r--r--docs/vyos.rest.vyos_firewall_interfaces_module.rst632
-rw-r--r--docs/vyos.rest.vyos_firewall_rules_module.rst725
-rw-r--r--docs/vyos.rest.vyos_user_module.rst393
-rw-r--r--plugins/modules/vyos_bgp_address_family.py631
-rw-r--r--plugins/modules/vyos_bgp_global.py572
-rw-r--r--plugins/modules/vyos_facts.py287
-rw-r--r--plugins/modules/vyos_firewall_global.py402
-rw-r--r--plugins/modules/vyos_firewall_interfaces.py473
-rw-r--r--plugins/modules/vyos_firewall_rules.py546
-rw-r--r--plugins/modules/vyos_user.py302
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/aliases1
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_populate_config.yaml32
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/deleted.yaml33
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/gathered.yaml21
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/merged.yaml65
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/overridden.yaml34
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/replaced.yaml34
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/tests/httpapi/rtt.yaml62
-rw-r--r--tests/integration/targets/vyos_bgp_address_family/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_bgp_global/aliases1
-rw-r--r--tests/integration/targets/vyos_bgp_global/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_bgp_global/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_bgp_global/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/_populate_config.yaml19
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/deleted.yaml29
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/gathered.yaml22
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/merged.yaml44
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/overridden.yaml35
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/replaced.yaml35
-rw-r--r--tests/integration/targets/vyos_bgp_global/tests/httpapi/rtt.yaml50
-rw-r--r--tests/integration/targets/vyos_bgp_global/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_facts/aliases1
-rw-r--r--tests/integration/targets/vyos_facts/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_facts/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_facts/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_facts/tests/httpapi/all.yaml16
-rw-r--r--tests/integration/targets/vyos_facts/tests/httpapi/default.yaml13
-rw-r--r--tests/integration/targets/vyos_facts/tests/httpapi/subsets.yaml33
-rw-r--r--tests/integration/targets/vyos_facts/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_firewall_global/aliases1
-rw-r--r--tests/integration/targets/vyos_firewall_global/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_firewall_global/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_global/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/_populate_config.yaml22
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/deleted.yaml29
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/gathered.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/merged.yaml48
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/overridden.yaml34
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/replaced.yaml34
-rw-r--r--tests/integration/targets/vyos_firewall_global/tests/httpapi/rtt.yaml55
-rw-r--r--tests/integration/targets/vyos_firewall_global/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/aliases1
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_populate_config.yaml23
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/deleted.yaml29
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/gathered.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/merged.yaml46
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/overridden.yaml33
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/replaced.yaml37
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/tests/httpapi/rtt.yaml53
-rw-r--r--tests/integration/targets/vyos_firewall_interfaces/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_firewall_rules/aliases1
-rw-r--r--tests/integration/targets/vyos_firewall_rules/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/_populate_config.yaml28
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/_remove_config.yaml5
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/deleted.yaml29
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/gathered.yaml21
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/merged.yaml50
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/overridden.yaml36
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/replaced.yaml36
-rw-r--r--tests/integration/targets/vyos_firewall_rules/tests/httpapi/rtt.yaml52
-rw-r--r--tests/integration/targets/vyos_firewall_rules/vars/main.yaml2
-rw-r--r--tests/integration/targets/vyos_user/aliases1
-rw-r--r--tests/integration/targets/vyos_user/defaults/main.yaml3
-rw-r--r--tests/integration/targets/vyos_user/tasks/httpapi.yaml21
-rw-r--r--tests/integration/targets/vyos_user/tasks/main.yaml5
-rw-r--r--tests/integration/targets/vyos_user/tests/httpapi/_remove_config.yaml8
-rw-r--r--tests/integration/targets/vyos_user/tests/httpapi/absent.yaml43
-rw-r--r--tests/integration/targets/vyos_user/tests/httpapi/gathered.yaml28
-rw-r--r--tests/integration/targets/vyos_user/tests/httpapi/present.yaml63
-rw-r--r--tests/integration/targets/vyos_user/tests/httpapi/rtt.yaml45
-rw-r--r--tests/integration/targets/vyos_user/vars/main.yaml2
-rw-r--r--tests/unit/fixtures/bgp_af_running.json29
-rw-r--r--tests/unit/fixtures/bgp_global_running.json27
-rw-r--r--tests/unit/fixtures/facts_interfaces.json11
-rw-r--r--tests/unit/fixtures/facts_system.json28
-rw-r--r--tests/unit/fixtures/firewall_global_running.json34
-rw-r--r--tests/unit/fixtures/firewall_interfaces_running.json30
-rw-r--r--tests/unit/fixtures/firewall_rules_running.json33
-rw-r--r--tests/unit/fixtures/user_running.json22
-rw-r--r--tests/unit/modules/test_vyos_bgp_address_family.py238
-rw-r--r--tests/unit/modules/test_vyos_bgp_global.py188
-rw-r--r--tests/unit/modules/test_vyos_facts.py97
-rw-r--r--tests/unit/modules/test_vyos_firewall_global.py190
-rw-r--r--tests/unit/modules/test_vyos_firewall_interfaces.py168
-rw-r--r--tests/unit/modules/test_vyos_firewall_rules.py203
-rw-r--r--tests/unit/modules/test_vyos_user.py172
113 files changed, 11028 insertions, 0 deletions
diff --git a/README.md b/README.md
index 108b42f..4c701af 100644
--- a/README.md
+++ b/README.md
@@ -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>&nbsp;&larr;</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>&nbsp;&larr;</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>&nbsp;&larr;</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>&nbsp;&larr;</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>&nbsp;&larr;</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>&nbsp;&larr;</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>&nbsp;&larr;</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()