summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-05-06 08:08:27 +1000
committeromnom62 <omnom62@outlook.com>2026-05-06 08:08:27 +1000
commit9a3fa5777e7203d46faf1185cfb56c4fc121d885 (patch)
tree80988400ba3582cfc97ff6445fb8f97c724d3fc6
parentc2493986714604aa992bea28628b74587b3b51bd (diff)
downloadrest.vyos-9a3fa5777e7203d46faf1185cfb56c4fc121d885.tar.gz
rest.vyos-9a3fa5777e7203d46faf1185cfb56c4fc121d885.zip
More modules
-rw-r--r--README.md37
-rw-r--r--docs/vyos.rest.vyos_banner_module.rst296
-rw-r--r--docs/vyos.rest.vyos_hostname_module.rst325
-rw-r--r--docs/vyos.rest.vyos_httpapi.rst18
-rw-r--r--docs/vyos.rest.vyos_image_module.rst227
-rw-r--r--docs/vyos.rest.vyos_logging_global_module.rst (renamed from docs/vyos.rest.vyos_snmp_server_module.rst)767
-rw-r--r--docs/vyos.rest.vyos_ntp_global_module.rst475
-rw-r--r--meta/runtime.yml60
-rw-r--r--plugins/README.md31
-rw-r--r--plugins/httpapi/vyos.py216
-rw-r--r--plugins/module_utils/utils.py64
-rw-r--r--plugins/module_utils/vyos.py231
-rw-r--r--plugins/module_utils/vyos_rest.py265
-rw-r--r--plugins/modules/vyos_banner.py428
-rw-r--r--plugins/modules/vyos_bgp_address_family.py354
-rw-r--r--plugins/modules/vyos_bgp_global.py365
-rw-r--r--plugins/modules/vyos_config_file.py130
-rw-r--r--plugins/modules/vyos_configure.py259
-rw-r--r--plugins/modules/vyos_facts.py161
-rw-r--r--plugins/modules/vyos_firewall_global.py360
-rw-r--r--plugins/modules/vyos_firewall_interfaces.py244
-rw-r--r--plugins/modules/vyos_firewall_rules.py492
-rw-r--r--plugins/modules/vyos_generate.py102
-rw-r--r--plugins/modules/vyos_hostname.py357
-rw-r--r--plugins/modules/vyos_image.py131
-rw-r--r--plugins/modules/vyos_info.py115
-rw-r--r--plugins/modules/vyos_interfaces.py401
-rw-r--r--plugins/modules/vyos_l3_interfaces.py381
-rw-r--r--plugins/modules/vyos_lag_interfaces.py228
-rw-r--r--plugins/modules/vyos_lldp_global.py151
-rw-r--r--plugins/modules/vyos_lldp_interfaces.py227
-rw-r--r--plugins/modules/vyos_logging_global.py482
-rw-r--r--plugins/modules/vyos_ntp_global.py358
-rw-r--r--plugins/modules/vyos_ospf_interfaces.py290
-rw-r--r--plugins/modules/vyos_ospfv2.py487
-rw-r--r--plugins/modules/vyos_ospfv3.py257
-rw-r--r--plugins/modules/vyos_ping.py180
-rw-r--r--plugins/modules/vyos_prefix_lists.py267
-rw-r--r--plugins/modules/vyos_reset.py108
-rw-r--r--plugins/modules/vyos_route_maps.py376
-rw-r--r--plugins/modules/vyos_show.py119
-rw-r--r--plugins/modules/vyos_snmp_server.py324
-rw-r--r--plugins/modules/vyos_static_routes.py397
-rw-r--r--plugins/modules/vyos_system.py147
-rw-r--r--plugins/modules/vyos_user.py282
-rw-r--r--plugins/modules/vyos_vlan.py197
-rw-r--r--tests/unit/test_vyos_rest_client.py150
47 files changed, 9245 insertions, 3074 deletions
diff --git a/README.md b/README.md
index 5a1d49e..baac392 100644
--- a/README.md
+++ b/README.md
@@ -6,22 +6,51 @@ Ansible collection for managing VyOS via REST API.
### Httpapi plugins
Name | Description
--- | ---
-[vyos.rest.vyos](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_httpapi.rst)|VyOS REST API
+[vyos.rest.vyos](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_httpapi.rst)|HttpApi plugin for VyOS REST API
### Modules
Name | Description
--- | ---
-[vyos.rest.vyos_banner](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_banner_module.rst)|Manage login banners 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 hostname of a VyOS device using REST API
+[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)|BGP Address Family resource module via 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 via the REST API.
+[vyos.rest.vyos_config_file](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_config_file_module.rst)|Save or load VyOS configuration files via the REST API.
+[vyos.rest.vyos_configure](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_configure_module.rst)|Manage VyOS device configuration via the 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 via REST API.
+[vyos.rest.vyos_firewall_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_firewall_global_module.rst)|Firewall global resource module via REST API.
+[vyos.rest.vyos_firewall_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_firewall_interfaces_module.rst)|Firewall interfaces resource module via 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 rules on VyOS via the REST API.
+[vyos.rest.vyos_generate](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_generate_module.rst)|Execute generate commands on a VyOS device via REST API.
+[vyos.rest.vyos_hostname](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_hostname_module.rst)|Manage the system hostname on a VyOS device via the REST API.
+[vyos.rest.vyos_image](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_image_module.rst)|Manage VyOS system images via the REST API.
+[vyos.rest.vyos_info](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_info_module.rst)|Retrieve system information from 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 attributes on a VyOS device via the 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 attributes on VyOS via the REST API.
+[vyos.rest.vyos_lag_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lag_interfaces_module.rst)|LAG/bonding interfaces resource module via REST API.
+[vyos.rest.vyos_lldp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_global_module.rst)|Manage LLDP global configuration on VyOS via REST API.
+[vyos.rest.vyos_lldp_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_interfaces_module.rst)|LLDP interfaces resource module via REST API.
+[vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API
[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API
+[vyos.rest.vyos_ospf_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospf_interfaces_module.rst)|OSPF Interfaces Resource Module via REST API.
+[vyos.rest.vyos_ospfv2](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospfv2_module.rst)|OSPFv2 resource module via REST API.
+[vyos.rest.vyos_ospfv3](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospfv3_module.rst)|OSPFv3 resource module via REST API.
+[vyos.rest.vyos_ping](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ping_module.rst)|Test reachability using ping via VyOS REST API.
+[vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|Prefix-Lists resource module via REST API.
+[vyos.rest.vyos_reset](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_reset_module.rst)|Execute reset commands on a VyOS device via REST API.
+[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Route Map resource module via REST API.
+[vyos.rest.vyos_show](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_show_module.rst)|Execute op-mode show commands on a VyOS device via 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 via the REST API.
+[vyos.rest.vyos_system](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_system_module.rst)|Manage system settings on VyOS via the REST API.
+[vyos.rest.vyos_user](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_user_module.rst)|Manage local users on VyOS via the REST API.
+[vyos.rest.vyos_vlan](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_vlan_module.rst)|Manage VLANs on VyOS network devices via REST API.
<!--end collection content-->
<!--start requires_ansible-->
## Ansible version compatibility
-This collection has been tested against the following Ansible versions: **>=2.16.0**.
+This collection has been tested against the following Ansible versions: **>=2.15.0**.
Plugins and modules within a collection may be tested with only specific Ansible versions.
A collection may contain metadata that identifies these versions.
diff --git a/docs/vyos.rest.vyos_banner_module.rst b/docs/vyos.rest.vyos_banner_module.rst
deleted file mode 100644
index 7e7b285..0000000
--- a/docs/vyos.rest.vyos_banner_module.rst
+++ /dev/null
@@ -1,296 +0,0 @@
-.. _vyos.rest.vyos_banner_module:
-
-
-*********************
-vyos.rest.vyos_banner
-*********************
-
-**Manage login banners on VyOS devices using REST API**
-
-
-Version added: 1.0.0
-
-.. contents::
- :local:
- :depth: 1
-
-
-Synopsis
---------
-- Configure pre-login and post-login banners on VyOS devices via REST API.
-- Supports idempotent configuration using structured data.
-- Multiline banner text is supported via YAML block scalars.
-- Uses REST API (``connection=httpapi``) instead of CLI.
-- VyOS stores multiline banners as a single string with literal ``\\n`` separators.
-
-
-
-
-Parameters
-----------
-
-.. raw:: html
-
- <table border=0 cellpadding=0 class="documentation-table">
- <tr>
- <th colspan="2">Parameter</th>
- <th>Choices/<font color="blue">Defaults</font></th>
- <th width="100%">Comments</th>
- </tr>
- <tr>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>config</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- / <span style="color: red">required</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Banner configuration.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>banner</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>pre-login</li>
- <li>post-login</li>
- </ul>
- </td>
- <td>
- <div>Banner type to configure.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>text</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>Banner text. Supports multiline strings via YAML block scalar (|).</div>
- <div>Internally, real newlines are converted to literal <code>\\n</code> as VyOS expects.</div>
- </td>
- </tr>
-
- <tr>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>state</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
- <li>replaced</li>
- <li>overridden</li>
- <li>deleted</li>
- <li>gathered</li>
- </ul>
- </td>
- <td>
- <div>Desired state of the configuration.</div>
- </td>
- </tr>
- </table>
- <br/>
-
-
-Notes
------
-
-.. note::
- - This module requires ``ansible_connection=httpapi``.
- - Banner text comparison is whitespace-normalized for idempotency.
- - VyOS stores banner text as a leaf string value with literal ``\\n`` for newlines.
- - For ``merged``, ``replaced``, and ``overridden`` states, the behaviour is identical for this single-leaf resource — the banner value is set to the desired text.
-
-
-
-Examples
---------
-
-.. code-block:: yaml
-
- - name: Configure single-line pre-login banner
- vyos.rest.vyos_banner:
- config:
- banner: pre-login
- text: "Unauthorized access is prohibited"
- state: merged
-
- - name: Configure multiline post-login banner
- vyos.rest.vyos_banner:
- config:
- banner: post-login
- text: |
- Welcome to VyOS
- Authorized users only
- Disconnect if you are not authorized
- state: merged
-
- - name: Replace post-login banner
- vyos.rest.vyos_banner:
- config:
- banner: post-login
- text: "Welcome to VyOS"
- state: replaced
-
- - name: Remove pre-login banner
- vyos.rest.vyos_banner:
- config:
- banner: pre-login
- state: deleted
-
- - name: Gather banner configuration
- vyos.rest.vyos_banner:
- config:
- banner: pre-login
- 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>Configuration after changes (text uses real newlines).</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;banner&#x27;: &#x27;pre-login&#x27;, &#x27;text&#x27;: &#x27;New banner text&#x27;}</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>before</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>always</td>
- <td>
- <div>Configuration before changes (text uses real newlines).</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;banner&#x27;: &#x27;pre-login&#x27;, &#x27;text&#x27;: &#x27;Old banner text&#x27;}</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>commands</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">list</span>
- </div>
- </td>
- <td>when changes are required</td>
- <td>
- <div>List of API command dicts sent to the device.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[{&#x27;op&#x27;: &#x27;set&#x27;, &#x27;path&#x27;: [&#x27;system&#x27;, &#x27;login&#x27;, &#x27;banner&#x27;, &#x27;pre-login&#x27;], &#x27;value&#x27;: &#x27;New banner text&#x27;}]</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>gathered</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>when state is gathered</td>
- <td>
- <div>Current device configuration (text uses real newlines).</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;banner&#x27;: &#x27;pre-login&#x27;, &#x27;text&#x27;: &#x27;Current banner text&#x27;}</div>
- </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 response from VyOS REST API.</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">dictionary</span>
- </div>
- </td>
- <td>when changes are applied</td>
- <td>
- <div>Result of save_config call after applying changes.</div>
- <br/>
- </td>
- </tr>
- </table>
- <br/><br/>
-
-
-Status
-------
-
-
-Authors
-~~~~~~~
-
-- Your Name (@yourhandle)
diff --git a/docs/vyos.rest.vyos_hostname_module.rst b/docs/vyos.rest.vyos_hostname_module.rst
deleted file mode 100644
index aa6e2ad..0000000
--- a/docs/vyos.rest.vyos_hostname_module.rst
+++ /dev/null
@@ -1,325 +0,0 @@
-.. _vyos.rest.vyos_hostname_module:
-
-
-***********************
-vyos.rest.vyos_hostname
-***********************
-
-**Manage the hostname of a VyOS device using REST API**
-
-
-Version added: 1.0.0
-
-.. contents::
- :local:
- :depth: 1
-
-
-Synopsis
---------
-- Configures the system hostname on VyOS network devices via the REST API.
-- Supports idempotent configuration — no change is made if the desired hostname already matches the running configuration.
-- Uses REST API (``connection=httpapi``) instead of CLI.
-
-
-
-
-Parameters
-----------
-
-.. raw:: html
-
- <table border=0 cellpadding=0 class="documentation-table">
- <tr>
- <th colspan="2">Parameter</th>
- <th>Choices/<font color="blue">Defaults</font></th>
- <th width="100%">Comments</th>
- </tr>
- <tr>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>config</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Hostname configuration dictionary.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>hostname</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>The hostname to set on the VyOS device.</div>
- <div>Must be a valid RFC 1123 hostname.</div>
- </td>
- </tr>
-
- <tr>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>state</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
- <li>replaced</li>
- <li>overridden</li>
- <li>deleted</li>
- <li>gathered</li>
- </ul>
- </td>
- <td>
- <div>The desired state of the hostname configuration.</div>
- <div><code>merged</code>, <code>replaced</code>, and <code>overridden</code> are identical for this single-value resource — all three ensure the hostname is set to the value specified in <code>config</code>.</div>
- <div><code>deleted</code> removes the configured hostname, reverting to the device default.</div>
- <div><code>gathered</code> retrieves the current hostname from the device and returns it as structured data in the <code>gathered</code> key. No changes are made to the device.</div>
- </td>
- </tr>
- </table>
- <br/>
-
-
-Notes
------
-
-.. note::
- - Tested against VyOS 1.3 (equuleus) and 1.4 (sagitta).
- - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin.
- - The ``ansible_network_os`` inventory variable must be set to ``vyos.rest.vyos``.
-
-
-
-Examples
---------
-
-.. code-block:: yaml
-
- # Before state:
- # -------------
- # vyos@router:~$ show configuration commands | grep host-name
- # set system host-name 'vyostest'
-
- - name: Set hostname using merged state
- vyos.rest.vyos_hostname:
- config:
- hostname: vyos
- state: merged
-
- # After state:
- # ------------
- # set system host-name 'vyos'
-
- # ------------------------------------------------------------------------
-
- # Before state:
- # -------------
- # set system host-name 'vyos'
-
- - name: Override hostname (identical behaviour to merged for this resource)
- vyos.rest.vyos_hostname:
- config:
- hostname: vyosTest
- state: overridden
-
- # After state:
- # ------------
- # set system host-name 'vyosTest'
-
- # ------------------------------------------------------------------------
-
- # Before state:
- # -------------
- # set system host-name 'vyos'
-
- - name: Replace hostname
- vyos.rest.vyos_hostname:
- config:
- hostname: vyosRouter
- state: replaced
-
- # After state:
- # ------------
- # set system host-name 'vyosRouter'
-
- # ------------------------------------------------------------------------
-
- # Before state:
- # -------------
- # set system host-name 'vyos'
-
- - name: Delete configured hostname
- vyos.rest.vyos_hostname:
- state: deleted
-
- # After state:
- # ------------
- # (no host-name entry — device reverts to default)
-
- # ------------------------------------------------------------------------
-
- - name: Gather current hostname from device
- vyos.rest.vyos_hostname:
- state: gathered
-
- # Module result:
- # --------------
- # "gathered": {
- # "hostname": "vyos"
- # }
-
- # ------------------------------------------------------------------------
-
- - name: Idempotency — no change when hostname already matches
- vyos.rest.vyos_hostname:
- config:
- hostname: vyos
- state: merged
-
- # Module result (hostname already 'vyos'):
- # ----------------------------------------
- # "changed": false
-
-
-
-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>Hostname configuration on the device after this module ran.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;hostname&#x27;: &#x27;vyos&#x27;}</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>before</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
- <td>
- <div>Hostname configuration on the device before this module ran.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;hostname&#x27;: &#x27;vyostest&#x27;}</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>commands</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">list</span>
- </div>
- </td>
- <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
- <td>
- <div>List of API command dicts sent to the device.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">[{&#x27;op&#x27;: &#x27;set&#x27;, &#x27;path&#x27;: [&#x27;system&#x27;, &#x27;host-name&#x27;, &#x27;vyos&#x27;]}]</div>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>gathered</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>when <em>state</em> is <code>gathered</code></td>
- <td>
- <div>Current hostname configuration retrieved from the device as structured data. Returned only when <em>state</em> is <code>gathered</code>.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;hostname&#x27;: &#x27;vyos&#x27;}</div>
- </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 response returned by the VyOS REST API.</div>
- <br/>
- <div style="font-size: smaller"><b>Sample:</b></div>
- <div style="font-size: smaller; color: blue; word-wrap: break-word; word-break: break-all;">{&#x27;success&#x27;: True, &#x27;data&#x27;: None, &#x27;error&#x27;: None}</div>
- </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">dictionary</span>
- </div>
- </td>
- <td>when changes are applied</td>
- <td>
- <div>Result of the save_config call issued after applying changes.</div>
- <br/>
- </td>
- </tr>
- </table>
- <br/><br/>
-
-
-Status
-------
-
-
-Authors
-~~~~~~~
-
-- Your Name (@yourhandle)
diff --git a/docs/vyos.rest.vyos_httpapi.rst b/docs/vyos.rest.vyos_httpapi.rst
index 7d887b5..e493ebc 100644
--- a/docs/vyos.rest.vyos_httpapi.rst
+++ b/docs/vyos.rest.vyos_httpapi.rst
@@ -5,9 +5,10 @@
vyos.rest.vyos
**************
-**VyOS REST API**
+**HttpApi plugin for VyOS REST API**
+Version added: 1.0.0
.. contents::
:local:
@@ -16,7 +17,9 @@ vyos.rest.vyos
Synopsis
--------
-- HTTPAPI plugin for interacting with VyOS REST API.
+- This HttpApi plugin provides methods to connect to VyOS devices via their HTTPS REST API.
+- Use with ``ansible_connection=ansible.netcommon.httpapi`` and ``ansible_network_os=vyos.rest.vyos``.
+- The VyOS REST API must be enabled with ``set service https api keys id ansible key YOUR_KEY``, ``set service https api rest``, then ``commit && save``.
@@ -45,14 +48,13 @@ Parameters
<td>
</td>
<td>
- <div> ini entries:
- <p>[httpapi]<br>api_key = VALUE</p>
- </div>
- <div>env:ANSIBLE_HTTPAPI_API_KEY</div>
+ <div>env:VYOS_API_KEY</div>
<div>var: ansible_httpapi_api_key</div>
+ <div>var: ansible_vyos_api_key</div>
</td>
<td>
- <div>VyOS API key</div>
+ <div>The API key configured on the VyOS device.</div>
+ <div>Set <code>ansible_httpapi_api_key</code> in inventory or the <code>VYOS_API_KEY</code> environment variable.</div>
</td>
</tr>
</table>
@@ -72,7 +74,7 @@ Status
Authors
~~~~~~~
-- Evgeny Molotkov (@eomnom62)
+- VyOS Community (@vyos)
.. hint::
diff --git a/docs/vyos.rest.vyos_image_module.rst b/docs/vyos.rest.vyos_image_module.rst
new file mode 100644
index 0000000..3dd396d
--- /dev/null
+++ b/docs/vyos.rest.vyos_image_module.rst
@@ -0,0 +1,227 @@
+.. _vyos.rest.vyos_image_module:
+
+
+********************
+vyos.rest.vyos_image
+********************
+
+**Manage VyOS system images via the REST API.**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- Adds or removes VyOS system images using the HTTPS REST API (``/image`` endpoint).
+- Use *state=present* to download and install an image from a URL.
+- Use *state=absent* to delete an installed image by name.
+
+
+
+Requirements
+------------
+The below requirements are needed on the host that executes this module.
+
+- VyOS 1.3+
+
+
+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>api_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>API key configured on the device.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>hostname</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>IP address or FQDN of the VyOS device.</div>
+ </td>
+ </tr>
+ <tr>
+ <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>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Version name of the image to remove (e.g. <code>1.4-rolling-202401010000</code>).</div>
+ <div>Required when <em>state=absent</em>.</div>
+ </td>
+ </tr>
+ <tr>
+ <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">integer</span>
+ </div>
+ </td>
+ <td>
+ <b>Default:</b><br/><div style="color: blue">443</div>
+ </td>
+ <td>
+ <div>HTTPS port for the REST API.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <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>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>present</li>
+ <li>absent</li>
+ </ul>
+ </td>
+ <td>
+ <div>{&#x27;<code>present</code>&#x27;: &#x27;Download and install the image from <em>url</em>.&#x27;}</div>
+ <div>{&#x27;<code>absent</code>&#x27;: &#x27;Delete the image specified by <em>name</em>.&#x27;}</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>timeout</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ <b>Default:</b><br/><div style="color: blue">300</div>
+ </td>
+ <td>
+ <div>Request timeout in seconds. Image downloads can take several minutes; increase this value accordingly.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>url</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>HTTP(S) URL of the VyOS <code>.iso</code> file to install.</div>
+ <div>Required when <em>state=present</em>.</div>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>verify_ssl</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><div style="color: blue"><b>no</b>&nbsp;&larr;</div></li>
+ <li>yes</li>
+ </ul>
+ </td>
+ <td>
+ <div>Validate the device&#x27;s TLS certificate.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+
+
+
+
+
+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>output</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>success</td>
+ <td>
+ <div>Text output from the image add/delete operation.</div>
+ <br/>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- VyOS Community (@vyos)
diff --git a/docs/vyos.rest.vyos_snmp_server_module.rst b/docs/vyos.rest.vyos_logging_global_module.rst
index 24651ec..718e646 100644
--- a/docs/vyos.rest.vyos_snmp_server_module.rst
+++ b/docs/vyos.rest.vyos_logging_global_module.rst
@@ -1,11 +1,11 @@
-.. _vyos.rest.vyos_snmp_server_module:
+.. _vyos.rest.vyos_logging_global_module:
-**************************
-vyos.rest.vyos_snmp_server
-**************************
+*****************************
+vyos.rest.vyos_logging_global
+*****************************
-**Manage SNMP server configuration on VyOS devices using REST API**
+**Manage syslog configuration on VyOS devices using REST API**
Version added: 1.0.0
@@ -17,8 +17,8 @@ Version added: 1.0.0
Synopsis
--------
-- Manages SNMP server configuration on VyOS devices via the REST API.
-- Supports communities, listen addresses, contact/location/description scalar fields, trap target, and SNMPv3 (engine ID, groups, users, views).
+- Manages syslog (logging) configuration on VyOS devices via the REST API.
+- Supports console, file, host, user, and global logging targets with per-target facility and severity configuration.
- Uses REST API (``connection=httpapi``) instead of CLI.
@@ -31,12 +31,12 @@ Parameters
<table border=0 cellpadding=0 class="documentation-table">
<tr>
- <th colspan="5">Parameter</th>
+ <th colspan="4">Parameter</th>
<th>Choices/<font color="blue">Defaults</font></th>
<th width="100%">Comments</th>
</tr>
<tr>
- <td colspan="5">
+ <td colspan="4">
<div class="ansibleOptionAnchor" id="parameter-"></div>
<b>config</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
@@ -47,123 +47,66 @@ Parameters
<td>
</td>
<td>
- <div>SNMP server configuration.</div>
+ <div>Logging configuration.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>communities</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>SNMP community configuration.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>authorization_type</b>
+ <b>console</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: purple">dictionary</span>
</div>
</td>
<td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>ro</li>
- <li>rw</li>
- </ul>
</td>
<td>
- <div>Authorization type.</div>
+ <div>Logging to serial console.</div>
</td>
</tr>
- <tr>
+ <tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>clients</b>
+ <b>facilities</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>
+ / <span style="color: purple">elements=dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>IP addresses of SNMP clients allowed to contact the system.</div>
</td>
</tr>
- <tr>
+ <tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>name</b>
+ <b>facility</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>Community 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>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=string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Subnets of SNMP clients allowed to contact the system.</div>
- </td>
- </tr>
-
- <tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>contact</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>Contact person for the system.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="4">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>description</b>
+ <b>severity</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -172,14 +115,15 @@ Parameters
<td>
</td>
<td>
- <div>System description.</div>
</td>
</tr>
+
+
<tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
+ <td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>listen_addresses</b>
+ <b>files</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">list</span>
@@ -189,33 +133,32 @@ Parameters
<td>
</td>
<td>
- <div>IP addresses to listen for incoming SNMP requests.</div>
+ <div>Logging to local files.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>address</b>
+ <b>archive</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>
+ <span style="color: purple">dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>IP address.</div>
</td>
</tr>
- <tr>
+ <tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>port</b>
+ <b>file_num</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">integer</span>
@@ -224,81 +167,32 @@ Parameters
<td>
</td>
<td>
- <div>UDP port (default 161).</div>
</td>
</tr>
-
<tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>location</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>System location.</div>
- </td>
- </tr>
- <tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>smux_peer</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>Register a subtree for SMUX-based processing.</div>
- </td>
- </tr>
- <tr>
<td class="elbow-placeholder"></td>
- <td colspan="4">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>snmp_v3</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>SNMPv3 configuration.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>engine_id</b>
+ <b>size</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: purple">integer</span>
</div>
</td>
<td>
</td>
<td>
- <div>EngineID as a hex string.</div>
</td>
</tr>
+
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="3">
+ <td colspan="2">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>groups</b>
+ <b>facilities</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">list</span>
@@ -308,79 +202,49 @@ Parameters
<td>
</td>
<td>
- <div>SNMPv3 groups.</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>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>Group name.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="2">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>mode</b>
+ <b>facility</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>ro</li>
- <li>rw</li>
- </ul>
</td>
<td>
- <div>Access mode.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="2">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>seclevel</b>
+ <b>severity</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>auth</li>
- <li>priv</li>
- </ul>
</td>
<td>
- <div>Security level.</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>view</b>
+ <b>path</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -389,53 +253,31 @@ Parameters
<td>
</td>
<td>
- <div>View name for this group.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>trap_targets</b>
+ <b>global_params</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>
+ <span style="color: purple">dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>SNMPv3 trap/inform targets.</div>
+ <div>Global syslog parameters (maps to <code>system syslog global</code>).</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>address</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>IP/IPv6 address of trap target.</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>authentication</b>
+ <b>archive</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">dictionary</span>
@@ -444,78 +286,32 @@ Parameters
<td>
</td>
<td>
- <div>Authentication for this trap target.</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>encrypted_key</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Encrypted authentication password.</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>plaintext_key</b>
+ <b>file_num</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: purple">integer</span>
</div>
</td>
<td>
</td>
<td>
- <div>Plaintext authentication password (will be encrypted on device).</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">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>md5</li>
- <li>sha</li>
- </ul>
- </td>
- <td>
- <div>Authentication protocol.</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>port</b>
+ <b>size</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">integer</span>
@@ -524,35 +320,33 @@ Parameters
<td>
</td>
<td>
- <div>TCP/UDP port for traps/informs.</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>privacy</b>
+ <b>facilities</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
- <span style="color: purple">dictionary</span>
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>Privacy for this trap target.</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>encrypted_key</b>
+ <b>facility</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -561,17 +355,15 @@ Parameters
<td>
</td>
<td>
- <div>Encrypted privacy password.</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>plaintext_key</b>
+ <b>severity</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -580,84 +372,51 @@ Parameters
<td>
</td>
<td>
- <div>Plaintext privacy password (will be encrypted on device).</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">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>des</li>
- <li>aes</li>
- </ul>
- </td>
- <td>
- <div>Privacy protocol.</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>
+ <b>marker_interval</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: purple">integer</span>
</div>
</td>
<td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>tcp</li>
- <li>udp</li>
- </ul>
</td>
<td>
- <div>Notification protocol.</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>type</b>
+ <b>preserve_fqdn</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: purple">boolean</span>
</div>
</td>
<td>
<ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>inform</li>
- <li>trap</li>
+ <li>no</li>
+ <li>yes</li>
</ul>
</td>
<td>
- <div>Notification type.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>users</b>
+ <b>hosts</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">list</span>
@@ -667,35 +426,33 @@ Parameters
<td>
</td>
<td>
- <div>SNMPv3 users.</div>
+ <div>Logging to remote syslog hosts.</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>authentication</b>
+ <b>facilities</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
- <span style="color: purple">dictionary</span>
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>Authentication configuration.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
<td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>encrypted_key</b>
+ <b>facility</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -704,17 +461,15 @@ Parameters
<td>
</td>
<td>
- <div>Encrypted authentication password.</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>plaintext_key</b>
+ <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>
@@ -723,197 +478,81 @@ Parameters
<td>
</td>
<td>
- <div>Plaintext authentication password (will be encrypted on device).</div>
+ <div>Per-facility protocol override.</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>
+ <b>severity</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>md5</li>
- <li>sha</li>
- </ul>
</td>
<td>
- <div>Authentication protocol.</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>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>Group membership for this user.</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>mode</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>ro</li>
- <li>rw</li>
- </ul>
- </td>
- <td>
- <div>Access mode.</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>privacy</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>Privacy configuration.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>encrypted_key</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Encrypted privacy password.</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>plaintext_key</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Plaintext privacy password (will be encrypted on device).</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>
+ <b>hostname</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>des</li>
- <li>aes</li>
- </ul>
</td>
<td>
- <div>Privacy protocol.</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>tsm_key</b>
+ <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>
+ <span style="color: purple">integer</span>
</div>
</td>
<td>
</td>
<td>
- <div>TSM certificate fingerprint or filename.</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>user</b>
+ <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>
</td>
<td>
- <div>Username.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
<td colspan="3">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>views</b>
+ <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>
@@ -923,34 +562,33 @@ Parameters
<td>
</td>
<td>
- <div>SNMPv3 views.</div>
+ <div>Logging to local user terminals.</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>exclude</b>
+ <b>facilities</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: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
</div>
</td>
<td>
</td>
<td>
- <div>Excluded OID subtree.</div>
</td>
</tr>
- <tr>
+ <tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="2">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>mask</b>
+ <b>facility</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -959,16 +597,15 @@ Parameters
<td>
</td>
<td>
- <div>Bit-mask for OID subidentifiers.</div>
</td>
</tr>
<tr>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
<td class="elbow-placeholder"></td>
- <td colspan="2">
+ <td colspan="1">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>oid</b>
+ <b>severity</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -977,35 +614,31 @@ Parameters
<td>
</td>
<td>
- <div>OID for this view.</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>view</b>
+ <b>username</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>View name.</div>
</td>
</tr>
<tr>
- <td class="elbow-placeholder"></td>
<td colspan="4">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>trap_source</b>
+ <b>running_config</b>
<a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
<div style="font-size: small">
<span style="color: purple">string</span>
@@ -1014,81 +647,12 @@ Parameters
<td>
</td>
<td>
- <div>SNMP trap source address.</div>
+ <div>Used only with state <code>parsed</code>.</div>
</td>
</tr>
<tr>
- <td class="elbow-placeholder"></td>
<td colspan="4">
<div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>trap_target</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>SNMP trap target.</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</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>IP address of trap target.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="3">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>community</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>Community string for traps.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="3">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>port</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">integer</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Destination port for trap notifications.</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">
@@ -1102,26 +666,18 @@ Parameters
<li>overridden</li>
<li>deleted</li>
<li>gathered</li>
+ <li>rendered</li>
+ <li>parsed</li>
</ul>
</td>
<td>
- <div>Desired state of SNMP configuration.</div>
- <div><code>merged</code> adds or updates provided config without removing existing entries.</div>
- <div><code>replaced</code> and <code>overridden</code> perform a full replacement — entries not in the task config are removed.</div>
- <div><code>deleted</code> removes all SNMP configuration with a single delete command.</div>
- <div><code>gathered</code> returns current device config as structured data, no changes made.</div>
+ <div>Desired state of the logging configuration.</div>
</td>
</tr>
</table>
<br/>
-Notes
------
-
-.. note::
- - Tested against VyOS 1.5q2.
-
Examples
@@ -1129,74 +685,50 @@ Examples
.. code-block:: yaml
- - name: Merge SNMP configuration
- vyos.rest.vyos_snmp_server:
+ - name: Merge logging configuration
+ vyos.rest.vyos_logging_global:
config:
- communities:
- - name: switches
- authorization_type: rw
- - name: bridges
- clients:
- - 1.1.1.1
- - 12.1.1.10
- contact: admin2@ex.com
- listen_addresses:
- - address: 20.1.1.1
- - address: 100.1.2.1
- port: 33
- snmp_v3:
- users:
- - user: admin_user
- authentication:
- plaintext_key: abc1234567
- type: sha
- privacy:
- plaintext_key: abc1234567
- type: aes
+ console:
+ facilities:
+ - facility: local7
+ severity: err
+ files:
+ - path: logFile
+ archive:
+ file_num: 2
+ facilities:
+ - facility: local6
+ severity: emerg
+ hosts:
+ - hostname: 172.16.0.1
+ port: 223
+ facilities:
+ - facility: local7
+ severity: all
+ - facility: all
+ protocol: udp
+ users:
+ - username: vyos
+ facilities:
+ - facility: local7
+ severity: debug
+ global_params:
+ archive:
+ file_num: 2
+ size: 111
+ facilities:
+ - facility: cron
+ severity: debug
+ marker_interval: 111
+ preserve_fqdn: true
state: merged
- # ------------------------------------------------------------------------
-
- - name: Replace SNMP configuration
- vyos.rest.vyos_snmp_server:
- config:
- communities:
- - name: bridges
- networks:
- - 1.1.1.0/24
- - 12.1.1.0/24
- location: "RDU, NC"
- listen_addresses:
- - address: 100.1.2.1
- port: 33
- snmp_v3:
- groups:
- - group: default
- view: default
- users:
- - user: admin_user
- authentication:
- encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633
- type: sha
- privacy:
- encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633
- type: aes
- group: default
- views:
- - view: default
- oid: "1"
- state: replaced
-
- # ------------------------------------------------------------------------
-
- - name: Delete all SNMP configuration
- vyos.rest.vyos_snmp_server:
+ - name: Delete all logging configuration
+ vyos.rest.vyos_logging_global:
state: deleted
- # ------------------------------------------------------------------------
-
- - name: Gather current SNMP configuration
- vyos.rest.vyos_snmp_server:
+ - name: Gather current logging configuration
+ vyos.rest.vyos_logging_global:
state: gathered
@@ -1224,7 +756,7 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
</td>
<td>when changed</td>
<td>
- <div>SNMP configuration after this module ran.</div>
+ <div>Logging configuration after this module ran.</div>
<br/>
</td>
</tr>
@@ -1237,9 +769,9 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
<span style="color: purple">dictionary</span>
</div>
</td>
- <td>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
+ <td>when state is merged, replaced, overridden or deleted</td>
<td>
- <div>SNMP configuration before this module ran.</div>
+ <div>Logging configuration before this module ran.</div>
<br/>
</td>
</tr>
@@ -1254,7 +786,7 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
</td>
<td>always</td>
<td>
- <div>List of API command dicts sent to the device.</div>
+ <div>List of API command tuples sent to the device.</div>
<br/>
</td>
</tr>
@@ -1267,24 +799,9 @@ Common return values are documented `here <https://docs.ansible.com/ansible/late
<span style="color: purple">dictionary</span>
</div>
</td>
- <td>when <em>state</em> is <code>gathered</code></td>
- <td>
- <div>Current SNMP 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>when state is gathered</td>
<td>
- <div>Raw API response.</div>
+ <div>Current logging configuration from the device.</div>
<br/>
</td>
</tr>
@@ -1314,4 +831,4 @@ Status
Authors
~~~~~~~
-- your_name (@yourhandle)
+- Sagar Paul (@KB-perByte)
diff --git a/docs/vyos.rest.vyos_ntp_global_module.rst b/docs/vyos.rest.vyos_ntp_global_module.rst
deleted file mode 100644
index 220797d..0000000
--- a/docs/vyos.rest.vyos_ntp_global_module.rst
+++ /dev/null
@@ -1,475 +0,0 @@
-.. _vyos.rest.vyos_ntp_global_module:
-
-
-*************************
-vyos.rest.vyos_ntp_global
-*************************
-
-**Manage NTP configuration on VyOS devices using REST API**
-
-
-Version added: 1.0.0
-
-.. contents::
- :local:
- :depth: 1
-
-
-Synopsis
---------
-- Manages NTP server, allow-client, and listen-address configuration on VyOS devices via the REST API.
-- Supports idempotent operation using structured data.
-- Uses REST API (``connection=httpapi``) instead of CLI.
-- Targets VyOS 1.4+ where NTP is managed by chronyd under ``service ntp``.
-
-
-
-
-Parameters
-----------
-
-.. raw:: html
-
- <table border=0 cellpadding=0 class="documentation-table">
- <tr>
- <th colspan="3">Parameter</th>
- <th>Choices/<font color="blue">Defaults</font></th>
- <th width="100%">Comments</th>
- </tr>
- <tr>
- <td colspan="3">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>config</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>NTP configuration.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>allow_clients</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>List of client networks or addresses allowed to query this NTP server.</div>
- <div>Maps to <code>service ntp allow-client address</code> on the device.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>listen_addresses</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">list</span>
- / <span style="color: purple">elements=string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Local IP addresses the NTP service should listen on.</div>
- <div>Maps to <code>service ntp listen-address</code> on the device.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td colspan="2">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>servers</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 upstream NTP servers to synchronise from.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>options</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">list</span>
- / <span style="color: purple">elements=string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li>dynamic</li>
- <li>noselect</li>
- <li>pool</li>
- <li>preempt</li>
- <li>prefer</li>
- <li>nts</li>
- <li>ptp</li>
- <li>interleave</li>
- </ul>
- </td>
- <td>
- <div>Per-server options.</div>
- <div><code>pool</code> replaces <code>dynamic</code> in VyOS 1.3+.</div>
- <div><code>nts</code> was added in VyOS 1.4.</div>
- <div><code>ptp</code> and <code>interleave</code> were added in VyOS 1.5.</div>
- <div><code>preempt</code> is only available in VyOS 1.3 and earlier.</div>
- </td>
- </tr>
- <tr>
- <td class="elbow-placeholder"></td>
- <td class="elbow-placeholder"></td>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>server</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>Server hostname or IP address.</div>
- </td>
- </tr>
-
-
- <tr>
- <td colspan="3">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>running_config</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- </td>
- <td>
- <div>Used only with state <code>parsed</code>.</div>
- <div>Provide the output of <code>show configuration commands | grep ntp</code> as a string. The module parses it into structured data and returns the result in the <code>parsed</code> key.</div>
- <div>No device connection is required for this state.</div>
- </td>
- </tr>
- <tr>
- <td colspan="3">
- <div class="ansibleOptionAnchor" id="parameter-"></div>
- <b>state</b>
- <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
- <div style="font-size: small">
- <span style="color: purple">string</span>
- </div>
- </td>
- <td>
- <ul style="margin: 0; padding: 0"><b>Choices:</b>
- <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
- <li>replaced</li>
- <li>overridden</li>
- <li>deleted</li>
- <li>gathered</li>
- <li>rendered</li>
- <li>parsed</li>
- </ul>
- </td>
- <td>
- <div>The desired state of the NTP configuration.</div>
- <div><code>merged</code> adds or updates the provided configuration without removing existing entries not mentioned in the task.</div>
- <div><code>replaced</code> fully replaces the running NTP configuration with the provided config, removing entries not present in the task.</div>
- <div><code>overridden</code> deletes all existing allow-client, listen-address, and server entries then applies the desired config from scratch.</div>
- <div><code>deleted</code> removes all NTP allow-client, listen-address, and server entries managed by this module.</div>
- <div><code>gathered</code> retrieves and returns the current NTP configuration as structured data. No changes are made to the device.</div>
- <div><code>rendered</code> returns the CLI commands that would be generated for the provided config without connecting to the device.</div>
- <div><code>parsed</code> parses the CLI output provided via <code>running_config</code> into structured data without connecting to the device.</div>
- </td>
- </tr>
- </table>
- <br/>
-
-
-Notes
------
-
-.. note::
- - Tested against VyOS 1.4 (sagitta) and 1.5.
- - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin.
- - ``ansible_network_os`` must be set to ``vyos.rest.vyos``.
- - ``replaced`` and ``overridden`` differ. ``replaced`` performs a surgical diff removing only entries not in the task. ``overridden`` deletes entire subtrees first then re-applies, which is safer when option ordering matters.
- - The ``rendered`` and ``parsed`` states do not require a device connection.
-
-
-
-Examples
---------
-
-.. code-block:: yaml
-
- # Before state:
- # -------------
- # set service ntp server time1.vyos.net
- # set service ntp server time2.vyos.net
- # set service ntp server time3.vyos.net
-
- - name: Merge NTP configuration
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.6.6.0/24
- listen_addresses:
- - 10.1.3.1
- servers:
- - server: 203.0.113.0
- options:
- - prefer
- state: merged
-
- # After state:
- # ------------
- # set service ntp allow-client address '10.6.6.0/24'
- # set service ntp listen-address '10.1.3.1'
- # set service ntp server 203.0.113.0 prefer
- # set service ntp server time1.vyos.net
- # set service ntp server time2.vyos.net
- # set service ntp server time3.vyos.net
-
- # ------------------------------------------------------------------------
-
- - name: Replace NTP configuration
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.6.6.0/24
- listen_addresses:
- - 10.1.3.1
- servers:
- - server: 203.0.113.0
- options:
- - prefer
- state: replaced
-
- # ------------------------------------------------------------------------
-
- - name: Override NTP configuration
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.3.3.0/24
- listen_addresses:
- - 10.7.8.1
- servers:
- - server: server1
- options:
- - dynamic
- - prefer
- - server: server2
- options:
- - noselect
- - preempt
- - server: serv
- state: overridden
-
- # ------------------------------------------------------------------------
-
- - name: Delete all managed NTP configuration
- vyos.rest.vyos_ntp_global:
- state: deleted
-
- # ------------------------------------------------------------------------
-
- - name: Gather current NTP configuration
- vyos.rest.vyos_ntp_global:
- state: gathered
-
- # ------------------------------------------------------------------------
-
- - name: Render NTP configuration commands offline
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.7.7.0/24
- - 10.8.8.0/24
- listen_addresses:
- - 10.7.9.1
- servers:
- - server: server7
- - server: server45
- options:
- - noselect
- - prefer
- - pool
- state: rendered
-
- # ------------------------------------------------------------------------
-
- - name: Parse NTP configuration from CLI output
- vyos.rest.vyos_ntp_global:
- running_config: "{{ lookup('file', './ntp_config.cfg') }}"
- state: parsed
-
-
-
-Return Values
--------------
-Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
-
-.. raw:: html
-
- <table border=0 cellpadding=0 class="documentation-table">
- <tr>
- <th colspan="1">Key</th>
- <th>Returned</th>
- <th width="100%">Description</th>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>after</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>when changed</td>
- <td>
- <div>NTP 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>when <em>state</em> is <code>merged</code>, <code>replaced</code>, <code>overridden</code> or <code>deleted</code></td>
- <td>
- <div>NTP configuration on the device 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 or dicts 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 <em>state</em> is <code>gathered</code></td>
- <td>
- <div>Current NTP configuration retrieved from the device as structured data.</div>
- <br/>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>parsed</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">dictionary</span>
- </div>
- </td>
- <td>when <em>state</em> is <code>parsed</code></td>
- <td>
- <div>Structured data parsed from the <code>running_config</code> CLI output.</div>
- <br/>
- </td>
- </tr>
- <tr>
- <td colspan="1">
- <div class="ansibleOptionAnchor" id="return-"></div>
- <b>rendered</b>
- <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
- <div style="font-size: small">
- <span style="color: purple">list</span>
- </div>
- </td>
- <td>when <em>state</em> is <code>rendered</code></td>
- <td>
- <div>CLI commands generated for the provided configuration (offline, no device needed).</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 from the VyOS REST API.</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>Result of save_config after applying changes.</div>
- <br/>
- </td>
- </tr>
- </table>
- <br/><br/>
-
-
-Status
-------
-
-
-Authors
-~~~~~~~
-
-- Varshitha Yataluru (@YVarshitha)
diff --git a/meta/runtime.yml b/meta/runtime.yml
index ceb0d37..04ed2da 100644
--- a/meta/runtime.yml
+++ b/meta/runtime.yml
@@ -1,11 +1,51 @@
-requires_ansible: ">=2.16.0"
+---
+requires_ansible: ">=2.15.0"
-# plugin_routing:
-
-# httpapi:
-# vyos:
-# redirect: vyos.rest.plugins.httpapi.vyos
-
-# modules:
-# vyos_ntp_global:
-# redirect: vyos.rest.plugins.modules.vyos_ntp_global
+# action_groups allow users to set shared connection params once using
+# module_defaults:
+#
+# - hosts: vyos_devices
+# module_defaults:
+# group/vyos.rest.api:
+# api_key: "{{ vyos_api_key }}"
+# verify_ssl: false
+#
+# This sets hostname/api_key/port/verify_ssl/timeout for ALL modules in
+# the group without repeating them on every task.
+action_groups:
+ api:
+ - vyos_banner
+ - vyos_bgp_address_family
+ - vyos_bgp_global
+ - vyos_config_file
+ - vyos_configure
+ - vyos_facts
+ - vyos_firewall_global
+ - vyos_firewall_interfaces
+ - vyos_firewall_rules
+ - vyos_generate
+ - vyos_hostname
+ - vyos_image
+ - vyos_info
+ - vyos_interfaces
+ - vyos_l3_interfaces
+ - vyos_lag_interfaces
+ - vyos_lldp_global
+ - vyos_lldp_interfaces
+ - vyos_logging_global
+ - vyos_ntp_global
+ - vyos_ospf_interfaces
+ - vyos_ospfv2
+ - vyos_ospfv3
+ - vyos_ping
+ - vyos_prefix_lists
+ - vyos_reboot
+ - vyos_reset
+ - vyos_retrieve
+ - vyos_route_maps
+ - vyos_show
+ - vyos_snmp_server
+ - vyos_static_routes
+ - vyos_system
+ - vyos_user
+ - vyos_vlan
diff --git a/plugins/README.md b/plugins/README.md
deleted file mode 100644
index 615f703..0000000
--- a/plugins/README.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Collections Plugins Directory
-
-This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
-is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
-would contain module utils and modules respectively.
-
-Here is an example directory of the majority of plugins currently supported by Ansible:
-
-```
-└── plugins
- ├── action
- ├── become
- ├── cache
- ├── callback
- ├── cliconf
- ├── connection
- ├── filter
- ├── httpapi
- ├── inventory
- ├── lookup
- ├── module_utils
- ├── modules
- ├── netconf
- ├── shell
- ├── strategy
- ├── terminal
- ├── test
- └── vars
-```
-
-A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.18/plugins/plugins.html).
diff --git a/plugins/httpapi/vyos.py b/plugins/httpapi/vyos.py
index 9340261..1719254 100644
--- a/plugins/httpapi/vyos.py
+++ b/plugins/httpapi/vyos.py
@@ -1,81 +1,173 @@
-import json
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
-from urllib.parse import urlencode
+from __future__ import absolute_import, division, print_function
-from ansible.plugins.httpapi import HttpApiBase
+__metaclass__ = type
DOCUMENTATION = r"""
---
-httpapi: vyos
-short_description: VyOS REST API
+name: vyos
+short_description: HttpApi plugin for VyOS REST API
description:
- - HTTPAPI plugin for interacting with VyOS REST API.
-author: Evgeny Molotkov (@eomnom62)
+ - This HttpApi plugin provides methods to connect to VyOS devices via their
+ HTTPS REST API.
+ - Use with C(ansible_connection=ansible.netcommon.httpapi) and
+ C(ansible_network_os=vyos.rest.vyos).
+ - The VyOS REST API must be enabled with
+ C(set service https api keys id ansible key YOUR_KEY),
+ C(set service https api rest), then C(commit && save).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
options:
api_key:
- description: VyOS API key
type: str
+ description:
+ - The API key configured on the VyOS device.
+ - Set C(ansible_httpapi_api_key) in inventory or the C(VYOS_API_KEY)
+ environment variable.
+ env:
+ - name: VYOS_API_KEY
vars:
- name: ansible_httpapi_api_key
- env:
- - name: ANSIBLE_HTTPAPI_API_KEY
- ini:
- - section: httpapi
- key: api_key
+ - name: ansible_vyos_api_key
"""
+import json
+import traceback
-class HttpApi(HttpApiBase):
-
- def set_options(self, task_keys=None, var_options=None, direct=None):
- super().set_options(
- task_keys=task_keys,
- var_options=var_options,
- direct=direct,
- )
-
- def send_request(self, path, data=None, method="POST", headers=None):
-
- api_key = self.get_option("api_key")
-
- if not api_key:
- # fallback to inventory vars
- api_key = self._play_context.vars.get("ansible_httpapi_api_key")
-
- if not api_key:
- raise ValueError("api_key is required")
-
- form_data = {}
-
- if data is not None:
- if not isinstance(data, str):
- data = json.dumps(data)
- form_data["data"] = data
-
- form_data["key"] = api_key
-
- body = urlencode(form_data).encode("utf-8")
-
- if headers is None:
- headers = {}
-
- headers["Content-Type"] = "application/x-www-form-urlencoded"
-
- status, response = self.connection.send(
- path,
- body,
- method=method,
- headers=headers,
- )
- if hasattr(response, "read"):
- response = response.read()
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode
- if isinstance(response, bytes):
- response = response.decode("utf-8")
+from ansible.errors import AnsibleConnectionFailure
+from ansible.module_utils._text import to_text
+from ansible.module_utils.connection import ConnectionError
+from ansible.plugins.httpapi import HttpApiBase
- if isinstance(response, str):
- response = json.loads(response)
- return response
+class HttpApi(HttpApiBase):
+ """HttpApi plugin for the VyOS HTTPS REST API."""
+
+ def login(self, username, password):
+ """VyOS uses a static API key — no login endpoint needed."""
+ pass
+
+ def logout(self):
+ pass
+
+ def update_auth(self, response, response_text):
+ return None
+
+ def handle_httperror(self, exc):
+ if exc.code == 401:
+ raise AnsibleConnectionFailure(
+ "VyOS API returned HTTP 401 Unauthorized. "
+ "Check ansible_httpapi_api_key is correct and that "
+ "'set service https api rest' is configured on the device.",
+ )
+ return exc
+
+ def _get_api_key(self):
+ """Read the API key — option, env var, or fail clearly."""
+ try:
+ key = self.get_option("api_key")
+ except Exception:
+ key = None
+ if not key:
+ import os
+
+ key = os.environ.get("VYOS_API_KEY", "")
+ if not key:
+ raise ConnectionError(
+ "No VyOS API key found. Set ansible_httpapi_api_key in "
+ "inventory or export VYOS_API_KEY=<key>.",
+ )
+ return key
+
+ def send_request(self, endpoint, **payload):
+ """POST to a VyOS REST endpoint.
+
+ Args:
+ endpoint (str): API path, e.g. '/configure' or '/retrieve'.
+ Named 'endpoint' not 'path' to avoid collision
+ with the VyOS payload field also called 'path'.
+ **payload: VyOS API fields: op, path, value, url, file, etc.
+
+ Returns:
+ dict: Parsed JSON response from VyOS.
+
+ Raises:
+ ConnectionError: on HTTP error or VyOS success=false response.
+ """
+ try:
+ api_key = self._get_api_key()
+ body = json.dumps(payload)
+ form_data = urlencode({"data": body, "key": api_key})
+
+ response, response_data = self.connection.send(
+ endpoint,
+ data=form_data,
+ method="POST",
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
+ )
+
+ raw = to_text(response_data.getvalue())
+
+ try:
+ result = json.loads(raw)
+ except ValueError:
+ raise ConnectionError(
+ "VyOS API at {ep} returned non-JSON ({code}): {raw}".format(
+ ep=endpoint,
+ code=getattr(response, "status", "?"),
+ raw=raw[:300],
+ ),
+ )
+
+ if not result.get("success"):
+ raise ConnectionError(
+ "VyOS API error [{ep}]: {err}".format(
+ ep=endpoint,
+ err=result.get("error") or "success=false",
+ ),
+ )
+
+ return result
+
+ except (ConnectionError, AnsibleConnectionFailure):
+ raise
+ except Exception as exc:
+ raise ConnectionError(
+ "{exc_type} in send_request({ep}): {exc}\n{tb}".format(
+ exc_type=type(exc).__name__,
+ ep=endpoint,
+ exc=to_text(exc),
+ tb=traceback.format_exc(),
+ ),
+ )
+
+ def get_info(self):
+ """GET /info — the one unauthenticated endpoint."""
+ try:
+ response, response_data = self.connection.send(
+ "/info",
+ data=None,
+ method="GET",
+ )
+ return json.loads(to_text(response_data.getvalue()))
+ except (ConnectionError, AnsibleConnectionFailure):
+ raise
+ except Exception as exc:
+ raise ConnectionError(
+ "{exc_type} in get_info(): {exc}\n{tb}".format(
+ exc_type=type(exc).__name__,
+ exc=to_text(exc),
+ tb=traceback.format_exc(),
+ ),
+ )
diff --git a/plugins/module_utils/utils.py b/plugins/module_utils/utils.py
index f8a738f..b54bd95 100644
--- a/plugins/module_utils/utils.py
+++ b/plugins/module_utils/utils.py
@@ -1,40 +1,58 @@
+"""
+Shared utility functions for the vyos.rest collection.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+
def normalize_to_list(value):
- """
- Normalize VyOS REST return values to list.
+ """Coerce any VyOS config value into a flat Python list.
- Handles:
- dict -> keys
- list -> same
- str -> [value]
- None -> []
- """
- if isinstance(value, dict):
- return list(value.keys())
+ VyOS returns config data in inconsistent shapes depending on how many
+ values are present:
+ - ``None`` -> no config exists -> ``[]``
+ - ``{}`` -> empty dict (node exists) -> ``[]``
+ - ``"10.0.0.1"`` -> single string value -> ``["10.0.0.1"]``
+ - ``["a", "b"]`` -> already a list -> ``["a", "b"]``
+ - ``{"a": {}, "b": {}}`` -> dict of leaf nodes -> ``["a", "b"]``
- if isinstance(value, list):
- return value
+ Args:
+ value: Raw value from a VyOS ``showConfig`` response.
+ Returns:
+ list: Flat list of string values.
+ """
+ if value is None:
+ return []
+ if isinstance(value, list):
+ return [str(v) for v in value]
if isinstance(value, str):
return [value]
-
- return []
+ if isinstance(value, dict):
+ # Keys are the values; dict values are sub-config (ignored here)
+ return [str(k) for k in value.keys()]
+ # Fallback: try to convert
+ return [str(value)]
def normalize_to_dict(value):
- """
- Normalize VyOS REST return values to dict.
+ """Coerce a VyOS multi-value node into a dict keyed by value.
- list -> {item: {}}
- str -> {value: {}}
- dict -> same
+ Args:
+ value: Raw value from showConfig (None, str, list, or dict).
+
+ Returns:
+ dict: ``{value: sub_config}`` pairs.
"""
+ if not value:
+ return {}
if isinstance(value, dict):
return value
-
if isinstance(value, list):
- return {v: {} for v in value}
-
+ return {str(v): {} for v in value}
if isinstance(value, str):
return {value: {}}
-
return {}
diff --git a/plugins/module_utils/vyos.py b/plugins/module_utils/vyos.py
index 469fbd3..87fc37b 100644
--- a/plugins/module_utils/vyos.py
+++ b/plugins/module_utils/vyos.py
@@ -1,104 +1,165 @@
-from ansible.module_utils.connection import Connection
+"""
+VyOSModule — high-level wrapper used by vyos.rest resource modules.
+
+Provides ``get_config()``, ``apply_commands()``, and ``save_config()``
+on top of ``VyOSRestClient``, so resource modules can work with simple
+``("set", path)`` / ``("delete", path)`` command tuples rather than
+calling the REST client directly.
+
+Version-adaptive paths
+----------------------
+Some VyOS config paths changed between minor versions (e.g. the NTP
+``allow-client address`` node was removed in 1.5+). ``apply_commands``
+handles this transparently: if a ``set`` command is rejected by the device
+it retries with the last path segment removed (one level up), covering
+the most common schema simplifications. A ``delete`` that fails is
+treated as a no-op (already absent).
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VyOSRestClient,
+ VyOSRestError,
+)
class VyOSModule:
+ """Thin wrapper around VyOSRestClient for resource modules.
+
+ Args:
+ module: AnsibleModule instance.
+ """
def __init__(self, module):
- self.module = module
-
- if not module._socket_path:
- module.fail_json(msg="Connection must be httpapi")
-
- self.connection = Connection(module._socket_path)
- self.existing = {}
-
- # ----------------------------------------------------------
- # Low-level transport
- # ----------------------------------------------------------
-
- def _send(self, path, op=None, path_list=None, commands=None):
- payload = {}
- if op:
- payload["op"] = op
- if path_list:
- payload["path"] = path_list
- if commands:
- payload["commands"] = commands
-
- response = self.connection.send_request(
- path=path,
- method="POST",
- data=payload,
- )
-
- if not response:
- self.module.fail_json(msg="Empty response from device")
-
- if not response.get("success"):
- error = response.get("error", "")
- # VyOS returns an error when path has no config — treat as empty
- if "empty" in error.lower() or "path is empty" in error.lower():
- return {}
- self.module.fail_json(
- msg="VyOS API error: {0}".format(error),
- response=response,
- )
+ self._module = module
+ self._client = VyOSRestClient(module)
- return response.get("data") or {}
+ # ------------------------------------------------------------------
+ # Read
+ # ------------------------------------------------------------------
- # ----------------------------------------------------------
- # Config retrieval
- # ----------------------------------------------------------
+ def get_config(self, path=None):
+ """Retrieve the configuration subtree at *path*.
- def get_config(self, path_list):
- """
- Fetch config at path_list via showConfig.
- Returns empty dict when path has no configuration.
+ Args:
+ path (list, optional): Config path tokens. ``[]`` or ``None``
+ returns the entire running configuration.
+
+ Returns:
+ dict: Configuration subtree, or ``{}`` if the path doesn't exist.
"""
- result = self._send(
- path="/retrieve",
- op="showConfig",
- path_list=path_list,
- )
- self.existing = result
- return result
-
- # ----------------------------------------------------------
- # Config application
- # ----------------------------------------------------------
+ try:
+ result = self._client.retrieve_show_config(path or [])
+ return result.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+ # ------------------------------------------------------------------
+ # Write
+ # ------------------------------------------------------------------
def apply_commands(self, commands):
+ """Execute a list of ``(op, path)`` command tuples against the device.
+
+ Args:
+ commands (list): Each item is a 2-tuple ``(op, path)`` where:
+ - *op* is ``"set"`` or ``"delete"``
+ - *path* is a list of config path tokens
+
+ Optionally a 3-tuple ``(op, path, value)`` for leaf nodes
+ that carry a value.
+
+ Returns:
+ list: Results from each command (dicts with ``op`` and ``path``).
+
+ Raises:
+ VyOSRestError: If a command fails and cannot be recovered.
+
+ Notes:
+ **Version-adaptive set:** if a ``set`` is rejected by the device,
+ the method retries once with the last path segment removed.
+ This handles schema simplifications between VyOS releases
+ (e.g. ``allow-client address <prefix>`` -> ``allow-client <prefix>``
+ in VyOS 1.5+).
+
+ **Idempotent delete:** ``delete`` failures are silently ignored
+ (the path is already absent).
"""
- Send a list of commands to /configure.
- Accepts tuples (op, path) or dicts {"op": ..., "path": ...}.
- Returns the raw API response dict.
- Raises via fail_json on API error.
- Does NOT call exit_json — the caller controls module exit.
- """
- if not commands:
- return {}
+ results = []
- payload_commands = []
for cmd in commands:
- if isinstance(cmd, tuple):
- op, path = cmd
- payload_commands.append({"op": op, "path": path})
- elif isinstance(cmd, dict):
- payload_commands.append(cmd)
+ # Support two command formats:
+ # Tuple: ("set", ["path", "tokens"]) or ("set", ["path"], "value")
+ # Dict: {"op": "set", "path": ["path", "tokens"]}
+ if isinstance(cmd, dict):
+ op = cmd["op"]
+ path = list(cmd["path"])
+ value = cmd.get("value")
+ else:
+ op = cmd[0]
+ path = list(cmd[1])
+ value = cmd[2] if len(cmd) > 2 else None
+
+ if op == "set":
+ results.append(self._apply_set(path, value))
+ elif op == "delete":
+ results.append(self._apply_delete(path))
else:
- self.module.fail_json(
- msg="Invalid command format: {0}".format(cmd),
+ self._module.fail_json(
+ msg="Unknown command op '{op}' in apply_commands".format(op=op),
)
- return self._send(
- path="/configure",
- commands=payload_commands,
- )
+ return results
+
+ def _apply_set(self, path, value=None):
+ """Set a config path, retrying with a shortened path on failure."""
+ try:
+ self._client.configure_set(path, value)
+ return {"op": "set", "path": path, "status": "ok"}
+ except VyOSRestError as exc:
+ # Retry without the second-to-last segment (version-adaptive).
+ # Example: ["service","ntp","allow-client","address","10.0.0.0/24"]
+ # -> ["service","ntp","allow-client","10.0.0.0/24"]
+ # Only attempt if path is long enough to have an intermediate node.
+ if len(path) >= 3:
+ short_path = path[:-2] + [path[-1]]
+ try:
+ self._client.configure_set(short_path, value)
+ return {"op": "set", "path": short_path, "status": "ok-adapted"}
+ except VyOSRestError:
+ pass
+ raise VyOSRestError(
+ "set {p} failed: {e}".format(p=" ".join(path), e=str(exc)),
+ )
- def save_config(self):
- """
- Persist running config to disk.
- Returns True on success, fails via fail_json on error.
+ def _apply_delete(self, path):
+ """Delete a config path; silently ignore if already absent."""
+ try:
+ self._client.configure_delete(path)
+ return {"op": "delete", "path": path, "status": "ok"}
+ except VyOSRestError:
+ return {"op": "delete", "path": path, "status": "noop"}
+
+ # ------------------------------------------------------------------
+ # Persist
+ # ------------------------------------------------------------------
+
+ def save_config(self, file_path=None):
+ """Save the running configuration to disk.
+
+ Args:
+ file_path (str, optional): Destination path on the device.
+ Defaults to ``/config/config.boot``.
+
+ Returns:
+ bool: ``True`` if saved successfully, ``False`` otherwise.
"""
- self._send(path="/config-file", op="save")
- return True
+ try:
+ self._client.config_file_save(file_path)
+ return True
+ except VyOSRestError:
+ return False
diff --git a/plugins/module_utils/vyos_rest.py b/plugins/module_utils/vyos_rest.py
new file mode 100644
index 0000000..95b3aac
--- /dev/null
+++ b/plugins/module_utils/vyos_rest.py
@@ -0,0 +1,265 @@
+"""
+VyOS REST API client utility — supports both connection modes.
+
+**httpapi mode (recommended)**::
+
+ # inventory
+ vyos15x ansible_host=192.168.122.194
+ ansible_network_os=vyos.rest.vyos
+ ansible_connection=ansible.netcommon.httpapi
+ ansible_httpapi_use_ssl=true
+ ansible_httpapi_port=443
+ ansible_httpapi_validate_certs=false
+ ansible_httpapi_api_key=vyos123
+
+ # playbook — no connection params needed on any task
+ - vyos.rest.vyos_banner:
+ banner: pre-login
+ text: "Authorised use only."
+ state: present
+
+**local mode (alternative, no inventory network_os required)**::
+
+ - vyos.rest.vyos_banner:
+ hostname: 192.168.122.194
+ api_key: vyos123
+ verify_ssl: false
+ banner: pre-login
+ text: "Authorised use only."
+ state: present
+
+Both modes call the same VyOS endpoints. The httpapi mode routes calls
+through the persistent connection daemon (better for large inventories);
+the local mode opens a new HTTPS connection per task.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import json
+import os
+
+
+try:
+ from urllib.parse import urlencode
+except ImportError:
+ from urllib import urlencode # Python 2
+
+try:
+ import urllib3
+
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+except ImportError:
+ pass
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.basic import env_fallback
+from ansible.module_utils.connection import Connection, ConnectionError
+from ansible.module_utils.urls import open_url
+
+
+class VyOSRestError(Exception):
+ """Raised when the VyOS REST API returns an error or is unreachable."""
+
+ pass
+
+
+class VyOSRestClient:
+ """Unified VyOS REST API client.
+
+ Automatically selects transport:
+ * **httpapi** when module._socket_path is set (ansible_connection=httpapi).
+ * **local HTTP** falls back to direct open_url calls otherwise.
+ """
+
+ def __init__(self, module):
+ self.module = module
+ socket_path = getattr(module, "_socket_path", None) or os.environ.get("ANSIBLE_SOCKET")
+ if socket_path and os.path.exists(socket_path):
+ self._mode = "httpapi"
+ self._conn = Connection(socket_path)
+ else:
+ self._mode = "local"
+ self._init_local()
+
+ def _init_local(self):
+ p = self.module.params
+ host = p.get("hostname") or ""
+ if not host:
+ self.module.fail_json(
+ msg=(
+ "No device address available. Either:\n"
+ " * Set ansible_connection=ansible.netcommon.httpapi with "
+ "ansible_network_os=vyos.rest.vyos in inventory (recommended), or\n"
+ " * Set the 'hostname' task parameter, or\n"
+ " * Export VYOS_HOST=<address>"
+ ),
+ )
+ self.hostname = host
+ self.port = p.get("port") or 443
+ self.api_key = p.get("api_key") or ""
+ self.timeout = p.get("timeout") or 30
+ self.verify_ssl = bool(p.get("verify_ssl"))
+ self.base_url = "https://{host}:{port}".format(host=self.hostname, port=self.port)
+
+ def _post(self, endpoint, payload):
+ if self._mode == "httpapi":
+ return self._post_httpapi(endpoint, payload)
+ return self._post_local(endpoint, payload)
+
+ def _post_httpapi(self, endpoint, payload):
+ try:
+ result = self._conn.send_request(endpoint=endpoint, **payload)
+ return result
+ except ConnectionError as exc:
+ raise VyOSRestError(str(exc))
+ except Exception as exc:
+ raise VyOSRestError(
+ "httpapi send_request to {ep} failed: {e}".format(ep=endpoint, e=str(exc)),
+ )
+
+ def _post_local(self, endpoint, payload):
+ url = "{base}{ep}".format(base=self.base_url, ep=endpoint)
+ body = json.dumps(payload)
+ form = urlencode({"data": body, "key": self.api_key})
+ headers = {"Content-Type": "application/x-www-form-urlencoded"}
+ try:
+ response = open_url(
+ url,
+ data=form,
+ headers=headers,
+ method="POST",
+ timeout=self.timeout,
+ validate_certs=self.verify_ssl,
+ )
+ raw = to_text(response.read())
+ except Exception as exc:
+ raise VyOSRestError("HTTP POST to {url} failed: {e}".format(url=url, e=str(exc)))
+ try:
+ result = json.loads(raw)
+ except ValueError:
+ raise VyOSRestError("Invalid JSON from {url}: {raw}".format(url=url, raw=raw))
+ if not result.get("success"):
+ raise VyOSRestError(
+ "VyOS API error at {url}: {err}".format(
+ url=url,
+ err=result.get("error", "unknown"),
+ ),
+ )
+ return result
+
+ def _get(self, endpoint, params=None):
+ if self._mode == "httpapi":
+ try:
+ return self._conn.get_info()
+ except Exception as exc:
+ raise VyOSRestError("httpapi get_info failed: {e}".format(e=str(exc)))
+ url = "{base}{ep}".format(base=self.base_url, ep=endpoint)
+ if params:
+ qs = "&".join("{k}={v}".format(k=k, v=v) for k, v in params.items())
+ url = "{url}?{qs}".format(url=url, qs=qs)
+ try:
+ response = open_url(
+ url,
+ method="GET",
+ timeout=self.timeout,
+ validate_certs=self.verify_ssl,
+ )
+ return json.loads(to_text(response.read()))
+ except Exception as exc:
+ raise VyOSRestError("HTTP GET to {url} failed: {e}".format(url=url, e=str(exc)))
+
+ # High-level helpers — identical interface regardless of transport
+ def configure_set(self, path, value=None):
+ payload = {"op": "set", "path": path}
+ if value is not None:
+ payload["value"] = value
+ return self._post("/configure", payload)
+
+ def configure_delete(self, path):
+ return self._post("/configure", {"op": "delete", "path": path})
+
+ def configure_comment(self, path, comment):
+ return self._post("/configure", {"op": "comment", "path": path, "value": comment})
+
+ def retrieve_show_config(self, path=None):
+ return self._post("/retrieve", {"op": "showConfig", "path": path or []})
+
+ def retrieve_return_values(self, path):
+ return self._post("/retrieve", {"op": "returnValues", "path": path})
+
+ def retrieve_return_value(self, path):
+ return self._post("/retrieve", {"op": "returnValue", "path": path})
+
+ def retrieve_exists(self, path):
+ result = self._post("/retrieve", {"op": "exists", "path": path})
+ return bool(result.get("data"))
+
+ def show(self, path):
+ return self._post("/show", {"op": "show", "path": path})
+
+ def generate(self, path):
+ return self._post("/generate", {"op": "generate", "path": path})
+
+ def reset(self, path):
+ return self._post("/reset", {"op": "reset", "path": path})
+
+ def reboot(self, at="now"):
+ return self._post("/reboot", {"op": "reboot", "path": [at]})
+
+ def poweroff(self, at="now"):
+ return self._post("/poweroff", {"op": "poweroff", "path": [at]})
+
+ def image_add(self, url):
+ return self._post("/image", {"op": "add", "url": url})
+
+ def image_delete(self, name):
+ return self._post("/image", {"op": "delete", "name": name})
+
+ def config_file_save(self, path=None):
+ payload = {"op": "save"}
+ if path:
+ payload["file"] = path
+ return self._post("/config-file", payload)
+
+ def config_file_load(self, path):
+ return self._post("/config-file", {"op": "load", "file": path})
+
+ def info(self, version=None, hostname=None):
+ params = {}
+ if version is not None:
+ params["version"] = str(version).lower()
+ if hostname is not None:
+ params["hostname"] = str(hostname).lower()
+ return self._get("/info", params or None)
+
+
+# Shared argument spec — only used in local mode; ignored when httpapi is active
+VYOS_REST_CONNECTION_ARGSPEC = dict(
+ hostname=dict(
+ type="str",
+ required=False,
+ default=None,
+ fallback=(env_fallback, ["VYOS_HOST"]),
+ ),
+ port=dict(
+ type="int",
+ default=443,
+ fallback=(env_fallback, ["VYOS_PORT"]),
+ ),
+ api_key=dict(
+ type="str",
+ required=False,
+ default=None,
+ no_log=True,
+ fallback=(env_fallback, ["VYOS_API_KEY"]),
+ ),
+ timeout=dict(type="int", default=30),
+ verify_ssl=dict(
+ type="bool",
+ default=False,
+ fallback=(env_fallback, ["VYOS_VERIFY_SSL"]),
+ ),
+)
diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py
index 4824629..b9df27d 100644
--- a/plugins/modules/vyos_banner.py
+++ b/plugins/modules/vyos_banner.py
@@ -1,33 +1,35 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+
+from __future__ import absolute_import, division, print_function
-from ansible.module_utils.basic import AnsibleModule
-from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+__metaclass__ = type
DOCUMENTATION = r"""
---
module: vyos_banner
-short_description: Manage login banners on VyOS devices using REST API
+short_description: Manage multiline banners on VyOS devices via REST API.
description:
- - Configure pre-login and post-login banners on VyOS devices via REST API.
- - Supports idempotent configuration using structured data.
- - Multiline banner text is supported via YAML block scalars.
- - Uses REST API (C(connection=httpapi)) instead of CLI.
- - VyOS stores multiline banners as a single string with literal C(\\n) separators.
+ - Manages pre-login and post-login banners on VyOS devices using the HTTPS
+ REST API.
+ - Works with C(ansible_connection=ansible.netcommon.httpapi) (recommended)
+ or with direct C(hostname)/C(api_key) task parameters.
+ - VyOS stores banner newlines as literal C(\n) in its config; this module
+ handles that conversion automatically.
version_added: "1.0.0"
author:
- - Your Name (@yourhandle)
-
+ - VyOS Community (@vyos)
options:
config:
description:
- Banner configuration.
type: dict
- required: true
suboptions:
banner:
description:
- - Banner type to configure.
+ - Which banner to configure.
type: str
required: true
choices:
@@ -35,53 +37,64 @@ options:
- post-login
text:
description:
- - Banner text. Supports multiline strings via YAML block scalar (|).
- - Internally, real newlines are converted to literal C(\\n) as VyOS expects.
+ - The banner text. Required when I(state=merged) or I(state=replaced).
+ - Use a YAML block scalar (C(|)) for multi-line banners.
+ - Real newline characters are converted to the C(\n) escape sequence
+ that VyOS stores internally; this is transparent to the user.
type: str
-
state:
description:
- - Desired state of the configuration.
+ - Desired state of the banner configuration.
+ - C(merged) - set the banner if it differs from the current value.
+ - C(replaced) - replace the banner text unconditionally.
+ - C(deleted) - remove the banner.
+ - C(gathered) - return the current banner in I(gathered) without
+ making any changes.
type: str
default: merged
choices:
- merged
- replaced
- - overridden
- deleted
- gathered
-
-notes:
- - This module requires C(ansible_connection=httpapi).
- - Banner text comparison is whitespace-normalized for idempotency.
- - VyOS stores banner text as a leaf string value with literal C(\\n) for newlines.
- - For C(merged), C(replaced), and C(overridden) states, the behaviour is identical
- for this single-leaf resource — the banner value is set to the desired text.
+ hostname:
+ description: Device IP or FQDN (not needed with httpapi inventory).
+ type: str
+ port:
+ description: HTTPS port (local mode only).
+ type: int
+ default: 443
+ api_key:
+ description: REST API key (not needed when ansible_httpapi_api_key is set).
+ type: str
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device TLS certificate.
+ type: bool
+ default: false
+seealso:
+ - module: vyos.vyos.vyos_banner
"""
EXAMPLES = r"""
-- name: Configure single-line pre-login banner
+- name: Set pre-login banner (merged — only changes if different)
vyos.rest.vyos_banner:
config:
banner: pre-login
- text: "Unauthorized access is prohibited"
- state: merged
-
-- name: Configure multiline post-login banner
- vyos.rest.vyos_banner:
- config:
- banner: post-login
text: |
- Welcome to VyOS
- Authorized users only
- Disconnect if you are not authorized
+ Junk pre-login banner
+ over multiple lines
state: merged
-- name: Replace post-login banner
+- name: Replace post-login banner unconditionally
vyos.rest.vyos_banner:
config:
banner: post-login
- text: "Welcome to VyOS"
+ text: "Welcome. Authorised access only."
state: replaced
- name: Remove pre-login banner
@@ -90,236 +103,84 @@ EXAMPLES = r"""
banner: pre-login
state: deleted
-- name: Gather banner configuration
+- name: Read current pre-login banner without changing it
vyos.rest.vyos_banner:
config:
banner: pre-login
state: gathered
+ register: result
+
+- name: Print gathered banner
+ ansible.builtin.debug:
+ msg: "Current banner: {{ result.gathered.text }}"
"""
RETURN = r"""
before:
- description: Configuration before changes (text uses real newlines).
+ description: Banner configuration before the module ran.
returned: always
type: dict
sample:
banner: pre-login
- text: "Old banner text"
-
+ text: "Old banner text\nline two\n"
after:
- description: Configuration after changes (text uses real newlines).
+ description: Banner configuration after the module ran.
returned: when changed
type: dict
- sample:
- banner: pre-login
- text: "New banner text"
-
-commands:
- description: List of API command dicts sent to the device.
- returned: when changes are required
- type: list
- sample:
- - op: set
- path: ["system", "login", "banner", "pre-login"]
- value: "New banner text"
-
gathered:
- description: Current device configuration (text uses real newlines).
+ description: Current banner configuration read from the device (state=gathered).
returned: when state is gathered
type: dict
sample:
banner: pre-login
- text: "Current banner text"
-
-response:
- description: Raw response from VyOS REST API.
- returned: when changes are applied
- type: dict
-
-saved:
- description: Result of save_config call after applying changes.
- returned: when changes are applied
- type: dict
+ text: "Current banner\nline two\n"
+commands:
+ description: Configuration commands issued.
+ returned: always
+ type: list
"""
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
-# ------------------------------------------------------------
-# Text conversion helpers
-#
-# Internal canonical form: real Python newlines (\n), stripped.
-#
-# VyOS wire format: literal backslash-n (\\n) sequences within
-# a single quoted string, e.g. 'line one\nline two\nline three'.
-#
-# Rule: convert FROM wire format on read, TO wire format on write,
-# always compare in canonical (internal) form.
-# ------------------------------------------------------------
-
-
-def normalize_text(text):
- """
- Normalize text to canonical internal form.
-
- - Converts to str if needed
- - Strips each line
- - Removes leading/trailing blank lines
- - Joins with real newlines
- - Returns None if result is empty
- """
- if text is None:
- return None
- lines = [line.strip() for line in text.strip().splitlines()]
- # Trim leading blank lines
- while lines and not lines[0]:
- lines.pop(0)
- # Trim trailing blank lines
- while lines and not lines[-1]:
- lines.pop()
- return "\n".join(lines) if lines else None
-
-
-def text_from_api_value(raw):
- """
- Convert VyOS API/device value to canonical internal form.
-
- VyOS returns multiline banners with literal \\n sequences:
- 'line one\\nline two\\nline three'
-
- Convert those to real newlines first, then normalize.
- """
- if raw is None:
- return None
- return normalize_text(raw.replace("\\n", "\n"))
-
-
-def text_to_api_value(text):
- """
- Convert canonical internal form to VyOS wire format.
-
- Real newlines become literal \\n sequences as VyOS expects.
- Returns None if input normalizes to None.
- """
- normalized = normalize_text(text)
- if normalized is None:
- return None
- return normalized.replace("\n", "\\n")
-
-
-# ------------------------------------------------------------
-# Device interaction
-# ------------------------------------------------------------
-
-
-def get_running_config(vyos, banner):
- """
- Fetch current banner configuration from the device.
-
- The VyOS showConfig API returns banner text as a plain string value:
- {"pre-login": "test1"}
- {"post-login": "line one\\nline two\\nline three"}
-
- Returns:
- {"banner": <str>, "text": <normalized str or None>}
- """
- try:
- raw = vyos.get_config(["system", "login", "banner"])
- except Exception as e:
- if "Configuration under specified path is empty" in str(e):
- return {"banner": banner, "text": None}
- raise
-
- if not raw or not isinstance(raw, dict):
- return {"banner": banner, "text": None}
-
- val = raw.get(banner)
-
- if val is None:
- return {"banner": banner, "text": None}
-
- if isinstance(val, str):
- # Convert from wire format (literal \n) to internal form (real newlines)
- return {"banner": banner, "text": text_from_api_value(val)}
-
- # Unexpected type — surface clearly rather than silently swallowing
- raise ValueError(
- "Unexpected banner value type '{t}' returned by API (value={v!r}). "
- "Expected a plain string. Please file a bug.".format(
- t=type(val).__name__,
- v=val,
- ),
- )
-
-
-def build_commands(want, have, state):
- """
- Build list of VyOS REST API command dicts.
-
- VyOS banner API payload structure:
- SET: {"op": "set", "path": ["system","login","banner","<type>"], "value": "<text>"}
- DELETE: {"op": "delete", "path": ["system","login","banner","<type>"]}
-
- The banner text is always a leaf VALUE on the path, never a path segment.
-
- All text comparison is done in canonical internal form (real newlines).
- Conversion to wire format (literal \\n) happens only when building the
- final API command value.
- """
- banner = want["banner"]
-
- # Canonical internal form for comparison
- want_text = normalize_text(want.get("text"))
- have_text = have.get("text") # already in canonical form from get_running_config
-
- base_path = ["system", "login", "banner", banner]
- banner_exists = have_text is not None
- # --------------------------------------------------------
- # deleted: remove the banner node if it exists
- # --------------------------------------------------------
- if state == "deleted":
- if banner_exists:
- return [{"op": "delete", "path": base_path}]
- return []
+_BANNER_PATH = {
+ "pre-login": ["system", "login", "banner", "pre-login"],
+ "post-login": ["system", "login", "banner", "post-login"],
+}
- # --------------------------------------------------------
- # merged / replaced / overridden
- #
- # For this single leaf-value resource all three states are
- # semantically equivalent: ensure the desired value is set.
- # Idempotency: skip if the normalized value already matches.
- # --------------------------------------------------------
- if state in ("merged", "replaced", "overridden"):
- if want_text is None:
- # No text provided with a non-delete state — nothing to do
- return []
- if want_text == have_text:
- # Already correct — idempotent, no change needed
- return []
+def _encode_banner(text):
+ """Convert real newlines to literal \\n for the VyOS REST API value field."""
+ return text.replace("\n", "\\n")
- # Convert to wire format only at the point of building the payload
- return [
- {
- "op": "set",
- "path": base_path,
- "value": text_to_api_value(want_text),
- },
- ]
- return []
+def _decode_banner(text):
+ """Convert literal \\n back to real newlines for display and comparison."""
+ return text.replace("\\n", "\n")
-# ------------------------------------------------------------
-# Main
-# ------------------------------------------------------------
+def _get_current(client, banner_type):
+ """Return current banner as a config dict, or empty dict if not set."""
+ path = _BANNER_PATH[banner_type]
+ try:
+ result = client.retrieve_return_value(path)
+ raw = result.get("data") or ""
+ if raw:
+ return {"banner": banner_type, "text": _decode_banner(raw)}
+ except VyOSRestError:
+ pass
+ return {"banner": banner_type, "text": ""}
def main():
-
argument_spec = dict(
config=dict(
type="dict",
- required=True,
options=dict(
banner=dict(
type="str",
@@ -330,86 +191,61 @@ def main():
),
),
state=dict(
+ type="str",
default="merged",
- choices=[
- "merged",
- "replaced",
- "overridden",
- "deleted",
- "gathered",
- ],
+ choices=["merged", "replaced", "deleted", "gathered"],
),
)
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
module = AnsibleModule(
argument_spec=argument_spec,
+ required_if=[
+ ("state", "merged", ["config"]),
+ ("state", "replaced", ["config"]),
+ ("state", "deleted", ["config"]),
+ ("state", "gathered", ["config"]),
+ ],
supports_check_mode=True,
)
- vyos = VyOSModule(module)
-
+ client = VyOSRestClient(module)
state = module.params["state"]
- config = module.params.get("config") or {}
- banner = config.get("banner")
+ config = module.params["config"]
+ banner_type = config["banner"]
+ desired_text = config.get("text") or ""
+ path = _BANNER_PATH[banner_type]
+ commands = []
+ changed = False
- if not banner:
- module.fail_json(msg="banner is required in config")
+ before = _get_current(client, banner_type)
- # Fetch current device state (text in canonical internal form)
- have = get_running_config(vyos, banner)
-
- # --------------------------------------------------------
- # gathered: return current state, no changes
- # --------------------------------------------------------
if state == "gathered":
- module.exit_json(changed=False, gathered=have)
-
- want = {
- "banner": banner,
- "text": config.get("text"),
- }
-
- commands = build_commands(want, have, state)
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
- # --------------------------------------------------------
- # check mode: report what would change, make no API calls
- # --------------------------------------------------------
if module.check_mode:
- module.exit_json(
- changed=bool(commands),
- commands=commands,
- before=have,
- )
-
- # --------------------------------------------------------
- # apply changes
- # --------------------------------------------------------
- if commands:
- response = vyos.apply_commands(commands)
- saved = vyos.save_config()
-
- module.exit_json(
- changed=True,
- before=have,
- # Report after-state in canonical form (real newlines, human-readable)
- after=want,
- # after={
- # "banner": banner,
- # "text": normalize_text(want["text"]),
- # },
- commands=commands,
- saved=saved,
- response=response,
- )
-
- # --------------------------------------------------------
- # no changes needed
- # --------------------------------------------------------
- module.exit_json(
- changed=False,
- before=have,
- after=have,
- )
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state in ("merged", "replaced"):
+ if state == "merged" and before.get("text") == desired_text:
+ module.exit_json(changed=False, before=before, commands=[])
+ client.configure_set(path, _encode_banner(desired_text))
+ commands.append("set {p} '...'".format(p=" ".join(path)))
+ changed = True
+
+ elif state == "deleted":
+ if not before.get("text"):
+ module.exit_json(changed=False, before=before, commands=[])
+ client.configure_delete(path)
+ commands.append("delete {p}".format(p=" ".join(path)))
+ changed = True
+
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc), before=before)
+
+ after = _get_current(client, banner_type) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
if __name__ == "__main__":
diff --git a/plugins/modules/vyos_bgp_address_family.py b/plugins/modules/vyos_bgp_address_family.py
new file mode 100644
index 0000000..ff80053
--- /dev/null
+++ b/plugins/modules/vyos_bgp_address_family.py
@@ -0,0 +1,354 @@
+#!/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: BGP Address Family resource module via REST API.
+description:
+ - Manages BGP address-family configuration (networks, redistribute,
+ aggregate-address, per-neighbor AFI settings) via the VyOS HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: BGP address-family configuration.
+ type: dict
+ suboptions:
+ as_number:
+ description: Local AS number.
+ type: int
+ required: true
+ address_family:
+ description: Global address-family parameters.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family (ipv4 or ipv6).
+ type: str
+ choices: [ipv4, ipv6]
+ aggregate_address:
+ description: List of BGP aggregate networks.
+ type: list
+ elements: dict
+ suboptions:
+ prefix:
+ type: str
+ as_set:
+ type: bool
+ summary_only:
+ type: bool
+ networks:
+ description: Networks to originate.
+ type: list
+ elements: dict
+ suboptions:
+ prefix:
+ type: str
+ path_limit:
+ type: int
+ backdoor:
+ type: bool
+ route_map:
+ type: str
+ redistribute:
+ description: Protocols to redistribute.
+ type: list
+ elements: dict
+ suboptions:
+ protocol:
+ type: str
+ choices: [connected, kernel, ospf, ospfv3, rip, ripng, static]
+ table:
+ type: str
+ route_map:
+ type: str
+ metric:
+ type: int
+ neighbors:
+ description: Per-neighbor address-family settings.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_address:
+ description: Neighbor IP.
+ type: str
+ address_family:
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ type: str
+ choices: [ipv4, ipv6]
+ allowas_in:
+ type: int
+ as_override:
+ type: bool
+ route_map:
+ description: Route-map settings.
+ type: dict
+ suboptions:
+ import_map:
+ type: str
+ export_map:
+ type: str
+ soft_reconfiguration:
+ type: bool
+ nexthop_self:
+ type: bool
+ remove_private_as:
+ type: bool
+ state:
+ description: Desired state.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+ hostname:
+ description: Device IP or FQDN.
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ description: Config before module ran.
+ returned: always
+ type: dict
+after:
+ description: Config after module ran.
+ returned: when changed
+ type: dict
+commands:
+ description: Commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def _get_bgp(client):
+ try:
+ result = client.retrieve_show_config(["protocols", "bgp"])
+ return result.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def _afi_unicast_key(afi):
+ return "ipv4-unicast" if afi == "ipv4" else "ipv6-unicast"
+
+
+def _apply(client, config, commands):
+ asn = str(config["as_number"])
+ base = ["protocols", "bgp", asn]
+
+ for af in config.get("address_family") or []:
+ afi = af["afi"]
+ af_base = base + ["address-family", _afi_unicast_key(afi)]
+
+ for agg in af.get("aggregate_address") or []:
+ p = agg["prefix"]
+ agg_base = af_base + ["aggregate-address", p]
+ client.configure_set(agg_base)
+ commands.append("set {b}".format(b=" ".join(agg_base)))
+ if agg.get("as_set"):
+ client.configure_set(agg_base + ["as-set"])
+ if agg.get("summary_only"):
+ client.configure_set(agg_base + ["summary-only"])
+
+ for net in af.get("networks") or []:
+ net_base = af_base + ["network", net["prefix"]]
+ client.configure_set(net_base)
+ commands.append("set {b}".format(b=" ".join(net_base)))
+ if net.get("route_map"):
+ client.configure_set(net_base + ["route-map"], net["route_map"])
+ if net.get("backdoor"):
+ client.configure_set(net_base + ["backdoor"])
+
+ for redist in af.get("redistribute") or []:
+ r_base = af_base + ["redistribute", redist["protocol"]]
+ client.configure_set(r_base)
+ commands.append("set {b}".format(b=" ".join(r_base)))
+ if redist.get("metric"):
+ client.configure_set(r_base + ["metric"], str(redist["metric"]))
+ if redist.get("route_map"):
+ client.configure_set(r_base + ["route-map"], redist["route_map"])
+
+ for nbr in config.get("neighbors") or []:
+ n_base = base + ["neighbor", nbr["neighbor_address"]]
+ for af in nbr.get("address_family") or []:
+ afi = af["afi"]
+ n_af_base = n_base + ["address-family", _afi_unicast_key(afi)]
+ client.configure_set(n_af_base)
+ commands.append("set {b}".format(b=" ".join(n_af_base)))
+ if af.get("allowas_in"):
+ client.configure_set(n_af_base + ["allowas-in", "number"], str(af["allowas_in"]))
+ if af.get("as_override"):
+ client.configure_set(n_af_base + ["as-override"])
+ if af.get("nexthop_self"):
+ client.configure_set(n_af_base + ["nexthop-self"])
+ if af.get("remove_private_as"):
+ client.configure_set(n_af_base + ["remove-private-as"])
+ if af.get("soft_reconfiguration"):
+ client.configure_set(n_af_base + ["soft-reconfiguration", "inbound"])
+ rm = af.get("route_map") or {}
+ if rm.get("import_map"):
+ client.configure_set(n_af_base + ["route-map", "import"], rm["import_map"])
+ if rm.get("export_map"):
+ client.configure_set(n_af_base + ["route-map", "export"], rm["export_map"])
+
+
+def main():
+ 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"]),
+ aggregate_address=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ prefix=dict(type="str"),
+ as_set=dict(type="bool"),
+ summary_only=dict(type="bool"),
+ ),
+ ),
+ networks=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ prefix=dict(type="str"),
+ path_limit=dict(type="int"),
+ backdoor=dict(type="bool"),
+ route_map=dict(type="str"),
+ ),
+ ),
+ redistribute=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ protocol=dict(
+ type="str",
+ choices=[
+ "connected",
+ "kernel",
+ "ospf",
+ "ospfv3",
+ "rip",
+ "ripng",
+ "static",
+ ],
+ ),
+ table=dict(type="str"),
+ route_map=dict(type="str"),
+ metric=dict(type="int"),
+ ),
+ ),
+ ),
+ ),
+ neighbors=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_address=dict(type="str"),
+ address_family=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"]),
+ allowas_in=dict(type="int"),
+ as_override=dict(type="bool"),
+ nexthop_self=dict(type="bool"),
+ remove_private_as=dict(type="bool"),
+ soft_reconfiguration=dict(type="bool"),
+ route_map=dict(
+ type="dict",
+ options=dict(
+ import_map=dict(type="str"),
+ export_map=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])],
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config")
+ commands = []
+ changed = False
+ before = _get_bgp(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ if before:
+ client.configure_delete(["protocols", "bgp"])
+ commands.append("delete protocols bgp")
+ changed = True
+ elif state in ("merged", "replaced"):
+ if state == "replaced" and before:
+ client.configure_delete(["protocols", "bgp"])
+ commands.append("delete protocols bgp")
+ _apply(client, config, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_bgp(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=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..6a08f4f
--- /dev/null
+++ b/plugins/modules/vyos_bgp_global.py
@@ -0,0 +1,365 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_bgp_global
+short_description: Manage BGP global configuration on VyOS via the REST API.
+description:
+ - Manages BGP global parameters on VyOS devices using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_bgp_global) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: BGP global configuration.
+ type: dict
+ suboptions:
+ as_number:
+ description: Local BGP AS number.
+ type: int
+ required: true
+ router_id:
+ description: BGP router ID (IPv4 address).
+ type: str
+ neighbors:
+ description: List of BGP neighbor configurations.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor:
+ description: Neighbor IP address.
+ type: str
+ required: true
+ remote_as:
+ description: Neighbor's AS number.
+ type: int
+ description:
+ description: Neighbor description.
+ type: str
+ password:
+ description: MD5 authentication password.
+ type: str
+ no_log: true
+ update_source:
+ description: Local interface or address for BGP session.
+ type: str
+ ebgp_multihop:
+ description: Maximum hops for eBGP sessions.
+ type: int
+ shutdown:
+ description: Whether to administratively shut down the neighbor.
+ type: bool
+ networks:
+ description: List of networks to originate.
+ type: list
+ elements: dict
+ suboptions:
+ prefix:
+ description: Network prefix to advertise.
+ type: str
+ required: true
+ route_map:
+ description: Route map to apply.
+ type: str
+ redistribute:
+ description: List of protocols to redistribute into BGP.
+ 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
+ state:
+ description:
+ - C(merged): Merge BGP config with existing.
+ - C(replaced): Replace entire BGP config.
+ - C(deleted): Remove BGP configuration.
+ - C(gathered): Read BGP config from device.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_bgp_global
+examples: |
+ - name: Configure BGP
+ vyos.rest.vyos_bgp_global:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ as_number: 65001
+ router_id: 192.168.1.1
+ neighbors:
+ - neighbor: 192.168.2.1
+ remote_as: 65002
+ description: "Peer AS65002"
+ networks:
+ - prefix: 10.0.0.0/8
+ redistribute:
+ - protocol: connected
+ state: merged
+
+ - name: Remove BGP configuration
+ vyos.rest.vyos_bgp_global:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: deleted
+"""
+
+RETURN = r"""
+before:
+ description: BGP config before the module ran.
+ returned: always
+ type: dict
+after:
+ description: BGP config after the module ran.
+ returned: when changed
+ type: dict
+gathered:
+ description: BGP config read from device (state=gathered).
+ returned: when state is gathered
+ type: dict
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_BGP_BASE = ["protocols", "bgp"]
+
+
+def _get_bgp(client):
+ try:
+ result = client.retrieve_show_config(_BGP_BASE)
+ return result.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def _apply_bgp(client, config, commands):
+ asn = str(config["as_number"])
+ base = _BGP_BASE + [asn]
+ client.configure_set(base)
+ commands.append("set protocols bgp {a}".format(a=asn))
+
+ if config.get("router_id"):
+ client.configure_set(base + ["parameters", "router-id"], config["router_id"])
+ commands.append(
+ "set protocols bgp {a} parameters router-id {r}".format(
+ a=asn,
+ r=config["router_id"],
+ ),
+ )
+
+ for nbr in config.get("neighbors") or []:
+ nbase = base + ["neighbor", nbr["neighbor"]]
+ client.configure_set(nbase)
+ commands.append("set protocols bgp {a} neighbor {n}".format(a=asn, n=nbr["neighbor"]))
+ if nbr.get("remote_as"):
+ client.configure_set(nbase + ["remote-as"], str(nbr["remote_as"]))
+ commands.append(
+ "set protocols bgp {a} neighbor {n} remote-as {r}".format(
+ a=asn,
+ n=nbr["neighbor"],
+ r=nbr["remote_as"],
+ ),
+ )
+ if nbr.get("description"):
+ client.configure_set(nbase + ["description"], nbr["description"])
+ if nbr.get("password"):
+ client.configure_set(nbase + ["password"], nbr["password"])
+ if nbr.get("update_source"):
+ client.configure_set(nbase + ["update-source"], nbr["update_source"])
+ if nbr.get("ebgp_multihop"):
+ client.configure_set(nbase + ["ebgp-multihop"], str(nbr["ebgp_multihop"]))
+ if nbr.get("shutdown"):
+ client.configure_set(nbase + ["shutdown"])
+ commands.append(
+ "set protocols bgp {a} neighbor {n} shutdown".format(
+ a=asn,
+ n=nbr["neighbor"],
+ ),
+ )
+
+ for net in config.get("networks") or []:
+ nbase = base + ["address-family", "ipv4-unicast", "network", net["prefix"]]
+ client.configure_set(nbase)
+ commands.append(
+ "set protocols bgp {a} address-family ipv4-unicast network {p}".format(
+ a=asn,
+ p=net["prefix"],
+ ),
+ )
+ if net.get("route_map"):
+ client.configure_set(nbase + ["route-map"], net["route_map"])
+
+ for redist in config.get("redistribute") or []:
+ rbase = base + [
+ "address-family",
+ "ipv4-unicast",
+ "redistribute",
+ redist["protocol"],
+ ]
+ client.configure_set(rbase)
+ commands.append(
+ "set protocols bgp {a} address-family ipv4-unicast redistribute {p}".format(
+ a=asn,
+ p=redist["protocol"],
+ ),
+ )
+ if redist.get("metric"):
+ client.configure_set(rbase + ["metric"], str(redist["metric"]))
+ if redist.get("route_map"):
+ client.configure_set(rbase + ["route-map"], redist["route_map"])
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ as_number=dict(type="int", required=True),
+ router_id=dict(type="str"),
+ neighbors=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor=dict(type="str", required=True),
+ remote_as=dict(type="int"),
+ description=dict(type="str"),
+ password=dict(type="str", no_log=True),
+ update_source=dict(type="str"),
+ ebgp_multihop=dict(type="int"),
+ shutdown=dict(type="bool"),
+ ),
+ ),
+ networks=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ prefix=dict(type="str", required=True),
+ route_map=dict(type="str"),
+ ),
+ ),
+ 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"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ("state", "merged", ["config"]),
+ ("state", "replaced", ["config"]),
+ ],
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config")
+ commands = []
+ changed = False
+
+ before = _get_bgp(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ if before:
+ client.configure_delete(_BGP_BASE)
+ commands.append("delete protocols bgp")
+ changed = True
+
+ elif state in ("merged", "replaced"):
+ if state == "replaced" and before:
+ client.configure_delete(_BGP_BASE)
+ commands.append("delete protocols bgp")
+ _apply_bgp(client, config, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_bgp(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_config_file.py b/plugins/modules/vyos_config_file.py
new file mode 100644
index 0000000..9d5e48a
--- /dev/null
+++ b/plugins/modules/vyos_config_file.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_config_file
+short_description: Save or load VyOS configuration files via the REST API.
+description:
+ - Manages VyOS configuration persistence via the HTTPS REST API
+ (C(/config-file) endpoint).
+ - Use I(op=save) to write the running configuration to a file.
+ - Use I(op=load) to load a configuration from a file and apply it.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ op:
+ description:
+ - C(save) - Save the running configuration to I(file) or the default C(/config/config.boot).
+ - C(load) - Load and commit the configuration from I(file).
+ type: str
+ choices: [save, load]
+ default: save
+ file:
+ description:
+ - Path on the VyOS device for save/load target.
+ - If omitted for C(save), the device uses its default boot config path.
+ type: str
+ hostname:
+ description:
+ - IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description:
+ - HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description:
+ - API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description:
+ - Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description:
+ - Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+"""
+
+RETURN = r"""
+output:
+ description: Text output from the config-file operation.
+ returned: success
+ type: str
+"""
+
+EXAMPLES = r"""
+- name: Save running config (default location)
+ vyos.rest.vyos_config_file:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ op: save
+
+- name: Save running config to a custom file
+ vyos.rest.vyos_config_file:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ op: save
+ file: /config/backups/config.boot.bak
+
+- name: Load a configuration from file
+ vyos.rest.vyos_config_file:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ op: load
+ file: /config/backups/config.boot.bak
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ op=dict(type="str", default="save", choices=["save", "load"]),
+ file=dict(type="str"),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("op", "load", ["file"])],
+ supports_check_mode=False,
+ )
+
+ client = VyOSRestClient(module)
+ op = module.params["op"]
+ file_path = module.params.get("file")
+
+ try:
+ if op == "save":
+ result = client.config_file_save(file_path)
+ else:
+ result = client.config_file_load(file_path)
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=True, output=result.get("data", ""))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_configure.py b/plugins/modules/vyos_configure.py
new file mode 100644
index 0000000..355a78c
--- /dev/null
+++ b/plugins/modules/vyos_configure.py
@@ -0,0 +1,259 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_configure
+short_description: Manage VyOS device configuration via the REST API.
+description:
+ - Sends one or more C(set) or C(delete) configuration commands to a VyOS
+ device via its HTTPS REST API (C(/configure) endpoint).
+ - Each command in I(commands) is executed and committed atomically.
+ - Supports Ansible check-mode (C(check_mode=yes)) - no changes are applied.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ commands:
+ description:
+ - List of VyOS configuration commands.
+ - Each item may be either a plain string in VyOS CLI syntax
+ (e.g. C(set interfaces ethernet eth0 description 'WAN'))
+ or a dict with keys C(op), C(path), and optionally C(value).
+ type: list
+ elements: raw
+ required: true
+ save:
+ description:
+ - Whether to save the configuration to disk after a successful commit.
+ type: bool
+ default: false
+ hostname:
+ description:
+ - IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description:
+ - HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description:
+ - API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description:
+ - Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description:
+ - Validate the device's TLS certificate.
+ type: bool
+ default: false
+notes:
+ - The VyOS HTTP API must be enabled on the device before using this module.
+ - Enable with C(set service https api keys id <ID> key <KEY>) and
+ C(set service https api rest), then C(commit && save).
+ - Tested against VyOS 1.3 and 1.4.
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.rest.vyos_retrieve
+ - module: vyos.rest.vyos_show"""
+
+RETURN = r"""
+commands_sent:
+ description: The list of commands dispatched to the API.
+ returned: always
+ type: list
+ elements: dict
+ sample:
+ - op: set
+ path: ["system", "host-name"]
+ value: vyos-router
+changed:
+ description: Whether the device configuration was modified.
+ returned: always
+ type: bool
+saved:
+ description: Whether the configuration was saved to disk.
+ returned: always
+ type: bool
+"""
+
+import re
+
+
+EXAMPLES = r"""
+- name: Set hostname via REST API
+ vyos.rest.vyos_configure:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ commands:
+ - "set system host-name vyos-router"
+
+- name: Configure an interface description and address
+ vyos.rest.vyos_configure:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ commands:
+ - "set interfaces ethernet eth1 description 'UPLINK'"
+ - "set interfaces ethernet eth1 address 10.0.0.1/30"
+ save: true
+
+- name: Delete a static route using dict syntax
+ vyos.rest.vyos_configure:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ commands:
+ - op: delete
+ path: ["protocols", "static", "route", "192.0.2.0/24"]
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def _parse_cli_command(cmd_str):
+ """Parse a VyOS CLI-syntax string into an API payload dict.
+
+ Supports:
+ set <path> [<value>]
+ delete <path>
+ comment <path> <comment>
+
+ Args:
+ cmd_str (str): e.g. "set interfaces ethernet eth0 description 'WAN'"
+
+ Returns:
+ dict: e.g. {"op": "set", "path": [...], "value": "WAN"}
+ """
+ cmd_str = cmd_str.strip()
+ op_match = re.match(r"^(set|delete|comment)\s+(.+)$", cmd_str, re.IGNORECASE)
+ if not op_match:
+ raise ValueError(
+ "Cannot parse command '{cmd}'. Must start with set/delete/comment.".format(
+ cmd=cmd_str,
+ ),
+ )
+ op = op_match.group(1).lower()
+ rest = op_match.group(2).strip()
+
+ # Extract a quoted or bare trailing value for 'set'
+ value = None
+ if op == "set":
+ # Look for a trailing quoted value: '...' or "..."
+ quoted = re.search(r"""(['"])(.*?)\1\s*$""", rest)
+ if quoted:
+ value = quoted.group(2)
+ rest = rest[: quoted.start()].strip()
+ else:
+ # Bare final token may be the value if the second-to-last token
+ # is a known leaf keyword – use a simple heuristic: if the
+ # last segment has no sub-tokens we treat it as value.
+ tokens = rest.split()
+ # We cannot know the schema here, so we pass the full path and
+ # no value; the device will decide.
+ path = tokens
+ return {"op": op, "path": path}
+
+ path = rest.split()
+ payload = {"op": op, "path": path}
+ if value is not None:
+ payload["value"] = value
+ return payload
+
+
+def _normalise_commands(raw_commands):
+ """Normalise the mixed list of str/dict commands into API dicts."""
+ result = []
+ for item in raw_commands:
+ if isinstance(item, dict):
+ if "op" not in item or "path" not in item:
+ raise ValueError(
+ "Command dict must have 'op' and 'path' keys: {item}".format(
+ item=item,
+ ),
+ )
+ result.append(item)
+ elif isinstance(item, str):
+ result.append(_parse_cli_command(item))
+ else:
+ raise ValueError(
+ "Command must be a string or dict, got: {t}".format(t=type(item)),
+ )
+ return result
+
+
+def main():
+ argument_spec = dict(
+ commands=dict(type="list", elements="raw", required=True),
+ save=dict(type="bool", default=False),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ try:
+ commands = _normalise_commands(module.params["commands"])
+ except ValueError as exc:
+ module.fail_json(msg=str(exc))
+
+ if module.check_mode:
+ module.exit_json(
+ changed=True,
+ commands_sent=commands,
+ saved=False,
+ msg="Check mode: no changes applied.",
+ )
+
+ client = VyOSRestClient(module)
+ changed = False
+ saved = False
+
+ try:
+ for cmd in commands:
+ op = cmd["op"].lower()
+ path = cmd["path"]
+ value = cmd.get("value")
+ if op == "set":
+ client.configure_set(path, value)
+ elif op == "delete":
+ client.configure_delete(path)
+ elif op == "comment":
+ client.configure_comment(path, value or "")
+ else:
+ module.fail_json(
+ msg="Unsupported op '{op}'".format(op=op),
+ )
+ changed = True
+
+ if module.params["save"]:
+ client.config_file_save()
+ saved = True
+
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc), commands_sent=commands)
+
+ module.exit_json(changed=changed, commands_sent=commands, saved=saved)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_facts.py b/plugins/modules/vyos_facts.py
new file mode 100644
index 0000000..66e684b
--- /dev/null
+++ b/plugins/modules/vyos_facts.py
@@ -0,0 +1,161 @@
+#!/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 via REST API.
+description:
+ - Collects facts from VyOS devices using the HTTPS REST API.
+ - Returns system info and optionally per-resource configuration facts.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ gather_subset:
+ description:
+ - Restrict facts to a subset. Use C(all), C(default), C(config), or C(min).
+ type: list
+ elements: str
+ default: ["min"]
+ gather_network_resources:
+ description:
+ - Which network resource facts to gather.
+ type: list
+ elements: str
+ available_network_resources:
+ description:
+ - When true, return a list of available resource names.
+ type: bool
+ default: false
+ hostname:
+ description:
+ - Device IP or FQDN.
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+ansible_facts:
+ description: Dictionary of facts keyed by resource name.
+ returned: always
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_NETWORK_RESOURCES = [
+ "interfaces",
+ "l3_interfaces",
+ "lag_interfaces",
+ "lldp_global",
+ "lldp_interfaces",
+ "static_routes",
+ "firewall_rules",
+ "firewall_global",
+ "firewall_interfaces",
+ "ospfv2",
+ "ospfv3",
+]
+
+_RESOURCE_PATHS = {
+ "interfaces": ["interfaces"],
+ "l3_interfaces": ["interfaces"],
+ "lag_interfaces": ["interfaces", "bonding"],
+ "lldp_global": ["service", "lldp"],
+ "lldp_interfaces": ["service", "lldp"],
+ "static_routes": ["protocols", "static"],
+ "firewall_rules": ["firewall"],
+ "firewall_global": ["firewall"],
+ "firewall_interfaces": ["firewall"],
+ "ospfv2": ["protocols", "ospf"],
+ "ospfv3": ["protocols", "ospfv3"],
+}
+
+
+def _get_subset(client, resource):
+ path = _RESOURCE_PATHS.get(resource, [])
+ try:
+ result = client.retrieve_show_config(path)
+ return result.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def main():
+ argument_spec = dict(
+ gather_subset=dict(type="list", elements="str", default=["min"]),
+ gather_network_resources=dict(type="list", elements="str"),
+ available_network_resources=dict(type="bool", default=False),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+
+ client = VyOSRestClient(module)
+ facts = {}
+
+ if module.params["available_network_resources"]:
+ facts["available_network_resources"] = _NETWORK_RESOURCES
+
+ gather_subset = module.params["gather_subset"]
+ get_all = "all" in gather_subset
+
+ # Always gather system info for "min" or "all"
+ if get_all or "min" in gather_subset or "default" in gather_subset:
+ try:
+ info = client.info()
+ facts["version"] = info.get("data", {}).get("version", "")
+ facts["hostname"] = info.get("data", {}).get("hostname", "")
+ except VyOSRestError:
+ pass
+
+ # Config subset
+ if get_all or "config" in gather_subset:
+ try:
+ result = client.retrieve_show_config([])
+ facts["config"] = result.get("data") or {}
+ except VyOSRestError:
+ facts["config"] = {}
+
+ # Network resources
+ requested = module.params.get("gather_network_resources") or []
+ if "all" in requested:
+ requested = _NETWORK_RESOURCES
+
+ for resource in requested:
+ if resource.startswith("!"):
+ continue
+ if resource in _NETWORK_RESOURCES:
+ facts["network_resources"] = facts.get("network_resources", {})
+ facts["network_resources"][resource] = _get_subset(client, resource)
+
+ module.exit_json(changed=False, 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..a3974fd
--- /dev/null
+++ b/plugins/modules/vyos_firewall_global.py
@@ -0,0 +1,360 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_firewall_global
+short_description: Firewall global resource module via REST API.
+description:
+ - Manages global VyOS firewall parameters (groups, ICMP redirect policy,
+ source validation, ping policy) via the HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: Global firewall configuration.
+ type: dict
+ suboptions:
+ validation:
+ description: Source-validation policy (strict/loose/disable).
+ type: str
+ choices: [strict, loose, disable]
+ config_trap:
+ description: SNMP trap on firewall config changes.
+ type: bool
+ ping:
+ description: ICMP echo policy.
+ type: dict
+ suboptions:
+ all:
+ type: bool
+ broadcast:
+ type: bool
+ route_redirects:
+ description: ICMP redirect / source-route options per AFI.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ icmp_redirects:
+ type: dict
+ suboptions:
+ send:
+ type: bool
+ receive:
+ type: bool
+ ip_src_route:
+ type: bool
+ group:
+ description: Firewall object groups.
+ type: dict
+ suboptions:
+ address_group:
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ required: true
+ description:
+ type: str
+ members:
+ description: List of IP addresses or ranges.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ afi:
+ type: str
+ default: ipv4
+ choices: [ipv4, ipv6]
+ network_group:
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ required: true
+ description:
+ type: str
+ members:
+ description: List of network prefixes.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ port_group:
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ required: true
+ description:
+ type: str
+ members:
+ description: List of port numbers or ranges.
+ type: list
+ elements: dict
+ suboptions:
+ port:
+ type: str
+ state:
+ description: Desired state.
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: dict
+after:
+ returned: when changed
+ type: dict
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_FW_BASE = ["firewall"]
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(_FW_BASE)
+ return r.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def _apply(client, config, commands):
+ if config.get("validation"):
+ path = (
+ _FW_BASE + ["ip", "src-route"]
+ if config["validation"] == "disable"
+ else _FW_BASE + ["ip", "enable-default-log"]
+ )
+ # Use the proper path
+ p = (
+ ["firewall", "global-options", "source-validation"]
+ if client.retrieve_exists(["firewall", "global-options"])
+ else ["firewall", "ip", "src-route"]
+ )
+ client.configure_set(["firewall", "ip", "source-validation"], config["validation"])
+ commands.append("set firewall ip source-validation {v}".format(v=config["validation"]))
+
+ if config.get("config_trap") is not None:
+ val = "enable" if config["config_trap"] else "disable"
+ client.configure_set(["firewall", "config-trap"], val)
+ commands.append("set firewall config-trap {v}".format(v=val))
+
+ ping = config.get("ping") or {}
+ if ping.get("all") is not None:
+ action = "enable" if ping["all"] else "disable"
+ client.configure_set(["firewall", "ip", "ping", "all"], action)
+ commands.append("set firewall ip ping all {a}".format(a=action))
+ if ping.get("broadcast") is not None:
+ action = "enable" if ping["broadcast"] else "disable"
+ client.configure_set(["firewall", "ip", "ping", "broadcast"], action)
+ commands.append("set firewall ip ping broadcast {a}".format(a=action))
+
+ for rr in config.get("route_redirects") or []:
+ afi = rr["afi"]
+ ip_key = "ip" if afi == "ipv4" else "ipv6"
+ icmp = rr.get("icmp_redirects") or {}
+ if icmp.get("send") is not None:
+ p = ["firewall", ip_key, "send-redirects"]
+ client.configure_set(p, "enable" if icmp["send"] else "disable")
+ commands.append(
+ "set {p} {v}".format(p=" ".join(p), v="enable" if icmp["send"] else "disable"),
+ )
+ if icmp.get("receive") is not None:
+ p = ["firewall", ip_key, "disable-forwarding"]
+ client.configure_set(p, "enable" if not icmp["receive"] else "disable")
+ if rr.get("ip_src_route") is not None:
+ p = ["firewall", ip_key, "source-route"]
+ client.configure_set(p, "enable" if rr["ip_src_route"] else "disable")
+
+ grp = config.get("group") or {}
+ for ag in grp.get("address_group") or []:
+ gpath = ["firewall", "group", "address-group", ag["name"]]
+ client.configure_set(gpath)
+ commands.append("set {p}".format(p=" ".join(gpath)))
+ if ag.get("description"):
+ client.configure_set(gpath + ["description"], ag["description"])
+ for m in ag.get("members") or []:
+ client.configure_set(gpath + ["address"], m["address"])
+ commands.append("set {p} address {a}".format(p=" ".join(gpath), a=m["address"]))
+
+ for ng in grp.get("network_group") or []:
+ gpath = ["firewall", "group", "network-group", ng["name"]]
+ client.configure_set(gpath)
+ commands.append("set {p}".format(p=" ".join(gpath)))
+ if ng.get("description"):
+ client.configure_set(gpath + ["description"], ng["description"])
+ for m in ng.get("members") or []:
+ client.configure_set(gpath + ["network"], m["address"])
+
+ for pg in grp.get("port_group") or []:
+ gpath = ["firewall", "group", "port-group", pg["name"]]
+ client.configure_set(gpath)
+ commands.append("set {p}".format(p=" ".join(gpath)))
+ if pg.get("description"):
+ client.configure_set(gpath + ["description"], pg["description"])
+ for m in pg.get("members") or []:
+ client.configure_set(gpath + ["port"], str(m["port"]))
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ validation=dict(type="str", choices=["strict", "loose", "disable"]),
+ config_trap=dict(type="bool"),
+ ping=dict(
+ type="dict",
+ options=dict(all=dict(type="bool"), broadcast=dict(type="bool")),
+ ),
+ route_redirects=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ icmp_redirects=dict(
+ type="dict",
+ options=dict(send=dict(type="bool"), receive=dict(type="bool")),
+ ),
+ ip_src_route=dict(type="bool"),
+ ),
+ ),
+ 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"),
+ afi=dict(type="str", default="ipv4"),
+ members=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ ),
+ ),
+ network_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ members=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ ),
+ ),
+ port_group=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ members=dict(
+ type="list",
+ elements="dict",
+ options=dict(port=dict(type="str")),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])],
+ supports_check_mode=True,
+ )
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config")
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ if before:
+ client.configure_delete(_FW_BASE)
+ commands.append("delete firewall")
+ changed = True
+ elif state in ("merged", "replaced"):
+ if state == "replaced" and before:
+ client.configure_delete(_FW_BASE)
+ commands.append("delete firewall")
+ _apply(client, config, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=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..d9085a6
--- /dev/null
+++ b/plugins/modules/vyos_firewall_interfaces.py
@@ -0,0 +1,244 @@
+#!/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: Firewall interfaces resource module via REST API.
+description:
+ - Attaches or detaches named firewall rule sets to/from interface
+ directions (in/out/local) using the VyOS HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of interface firewall attachment configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Interface name.
+ type: str
+ required: true
+ access_rules:
+ description: Firewall rule sets per AFI.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ rules:
+ description: Rule sets and directions.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Rule set name.
+ type: str
+ direction:
+ description: Traffic direction.
+ type: str
+ choices: [in, local, out]
+ required: true
+ state:
+ description: Desired state.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_IFACE_TYPES = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "vti": "vti",
+ "vxlan": "vxlan",
+ "lo": "loopback",
+ "dummy": "dummy",
+ "br": "bridge",
+ "wg": "wireguard",
+ "tun": "tunnel",
+}
+
+
+def _itype(name):
+ for p, t in _IFACE_TYPES.items():
+ if name.startswith(p):
+ return t
+ return "ethernet"
+
+
+def _fw_direction_key(afi, direction):
+ if afi == "ipv4":
+ return {"in": "in", "out": "out", "local": "local"}[direction]
+ return {"in": "in", "out": "out", "local": "local"}[direction]
+
+
+def _apply(client, iface_cfg, commands):
+ name = iface_cfg["name"]
+ itype = _itype(name)
+ ibase = ["interfaces", itype, name, "firewall"]
+
+ for ar in iface_cfg.get("access_rules") or []:
+ afi = ar["afi"]
+ fw_key = "firewall" if afi == "ipv4" else "ipv6"
+ for rule in ar.get("rules") or []:
+ direction = rule["direction"]
+ rs_name = rule.get("name")
+ if rs_name:
+ path = ibase + [direction, fw_key if afi == "ipv6" else "name"]
+ client.configure_set(path, rs_name)
+ commands.append(
+ "set interfaces {t} {n} firewall {d} {k} '{rs}'".format(
+ t=itype,
+ n=name,
+ d=direction,
+ k="ipv6-name" if afi == "ipv6" else "name",
+ rs=rs_name,
+ ),
+ )
+
+
+def _delete_iface_fw(client, name, commands):
+ itype = _itype(name)
+ path = ["interfaces", itype, name, "firewall"]
+ try:
+ client.configure_delete(path)
+ commands.append("delete interfaces {t} {n} firewall".format(t=itype, n=name))
+ except VyOSRestError:
+ pass
+
+
+def _get(client):
+ try:
+ result = client.retrieve_show_config(["interfaces"])
+ raw = result.get("data") or {}
+ out = []
+ for itype, itype_data in raw.items():
+ if not isinstance(itype_data, dict):
+ continue
+ for iname, idata in itype_data.items():
+ fw = idata.get("firewall") if isinstance(idata, dict) else None
+ if fw:
+ out.append({"name": iname, "firewall": fw})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ access_rules=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ rules=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str"),
+ direction=dict(
+ type="str",
+ required=True,
+ choices=["in", "local", "out"],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ targets = {i["name"] for i in config} if config else {i["name"] for i in before}
+ for iface in before:
+ if iface["name"] in targets:
+ _delete_iface_fw(client, iface["name"], commands)
+ changed = True
+ elif state in ("merged", "replaced", "overridden"):
+ if state in ("replaced", "overridden"):
+ for cfg in config:
+ _delete_iface_fw(client, cfg["name"], commands)
+ for cfg in config:
+ _apply(client, cfg, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=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..f535f3a
--- /dev/null
+++ b/plugins/modules/vyos_firewall_rules.py
@@ -0,0 +1,492 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_firewall_rules
+short_description: Manage firewall rules on VyOS via the REST API.
+description:
+ - Manages VyOS firewall rule sets and individual rules using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_firewall_rules) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of firewall rule set configurations.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ rule_sets:
+ description: List of named rule sets.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Rule set name.
+ type: str
+ required: true
+ default_action:
+ description: Default action for unmatched traffic.
+ type: str
+ choices: [drop, reject, accept]
+ default: drop
+ description:
+ description: Rule set description.
+ type: str
+ rules:
+ description: List of firewall rules.
+ type: list
+ elements: dict
+ suboptions:
+ number:
+ description: Rule number (1-999999).
+ type: int
+ required: true
+ action:
+ description: Rule action.
+ type: str
+ choices: [drop, reject, accept, inspect]
+ description:
+ description: Rule description.
+ type: str
+ protocol:
+ description: Protocol (e.g. C(tcp), C(udp), C(icmp), C(all)).
+ type: str
+ source:
+ description: Source match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Source IP address or network.
+ type: str
+ port:
+ description: Source port or port range.
+ type: str
+ group:
+ description: Address or network group.
+ type: dict
+ suboptions:
+ address_group:
+ type: str
+ network_group:
+ type: str
+ destination:
+ description: Destination match criteria.
+ type: dict
+ suboptions:
+ address:
+ description: Destination IP address or network.
+ type: str
+ port:
+ description: Destination port or port range.
+ type: str
+ group:
+ description: Address or network group.
+ type: dict
+ suboptions:
+ address_group:
+ type: str
+ network_group:
+ type: str
+ state:
+ description: Connection state matching.
+ type: dict
+ suboptions:
+ established:
+ type: bool
+ new:
+ type: bool
+ related:
+ type: bool
+ invalid:
+ type: bool
+ enabled:
+ description: Whether the rule is active.
+ type: bool
+ default: true
+ state:
+ description:
+ - C(merged): Add/update listed rule sets and rules.
+ - C(replaced): Replace listed rule sets entirely.
+ - C(overridden): Replace the entire firewall rule config.
+ - C(deleted): Remove listed (or all) rule sets.
+ - C(gathered): Read firewall rule config from device.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_firewall_rules
+examples: |
+ - name: Create an IPv4 firewall rule set
+ vyos.rest.vyos_firewall_rules:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - afi: ipv4
+ rule_sets:
+ - name: OUTSIDE-IN
+ default_action: drop
+ rules:
+ - number: 10
+ action: accept
+ protocol: tcp
+ destination:
+ port: "80,443"
+ state:
+ established: true
+ related: true
+ state: merged
+
+ - name: Delete all firewall rules
+ vyos.rest.vyos_firewall_rules:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: deleted
+"""
+
+RETURN = r"""
+before:
+ description: Firewall rule config before the module ran.
+ returned: always
+ type: list
+after:
+ description: Firewall rule config after the module ran.
+ returned: when changed
+ type: list
+gathered:
+ description: Firewall config read from device (state=gathered).
+ returned: when state is gathered
+ type: list
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_FW_PATH = {
+ "ipv4": ["firewall", "name"],
+ "ipv6": ["firewall", "ipv6-name"],
+}
+
+
+def _get_fw_rules(client):
+ try:
+ result = client.retrieve_show_config(["firewall"])
+ data = result.get("data") or {}
+ out = []
+ for afi, key in [("ipv4", "name"), ("ipv6", "ipv6-name")]:
+ sets_data = data.get(key, {})
+ if not isinstance(sets_data, dict):
+ continue
+ rule_sets = []
+ for rs_name, rs_data in sets_data.items():
+ if not isinstance(rs_data, dict):
+ continue
+ rs = {
+ "name": rs_name,
+ "default_action": rs_data.get("default-action", "drop"),
+ }
+ if "description" in rs_data:
+ rs["description"] = rs_data["description"]
+ rules = []
+ for rnum, rdata in (rs_data.get("rule") or {}).items():
+ if not isinstance(rdata, dict):
+ continue
+ rule = {"number": int(rnum)}
+ if "action" in rdata:
+ rule["action"] = rdata["action"]
+ if "description" in rdata:
+ rule["description"] = rdata["description"]
+ if "protocol" in rdata:
+ rule["protocol"] = rdata["protocol"]
+ rule["enabled"] = "disable" not in rdata
+ rules.append(rule)
+ if rules:
+ rs["rules"] = sorted(rules, key=lambda r: r["number"])
+ rule_sets.append(rs)
+ if rule_sets:
+ out.append({"afi": afi, "rule_sets": rule_sets})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply_rule_set(client, afi, rs, commands):
+ base = _FW_PATH[afi] + [rs["name"]]
+ client.configure_set(base)
+ commands.append("set {p}".format(p=" ".join(base)))
+
+ if rs.get("default_action"):
+ client.configure_set(base + ["default-action"], rs["default_action"])
+ commands.append(
+ "set {p} default-action {a}".format(
+ p=" ".join(base),
+ a=rs["default_action"],
+ ),
+ )
+ if rs.get("description"):
+ client.configure_set(base + ["description"], rs["description"])
+ commands.append(
+ "set {p} description '{d}'".format(
+ p=" ".join(base),
+ d=rs["description"],
+ ),
+ )
+
+ for rule in rs.get("rules") or []:
+ rbase = base + ["rule", str(rule["number"])]
+ client.configure_set(rbase)
+ commands.append("set {p}".format(p=" ".join(rbase)))
+
+ if rule.get("action"):
+ client.configure_set(rbase + ["action"], rule["action"])
+ commands.append(
+ "set {p} action {a}".format(p=" ".join(rbase), a=rule["action"]),
+ )
+ if rule.get("description"):
+ client.configure_set(rbase + ["description"], rule["description"])
+ if rule.get("protocol"):
+ client.configure_set(rbase + ["protocol"], rule["protocol"])
+ commands.append(
+ "set {p} protocol {pr}".format(
+ p=" ".join(rbase),
+ pr=rule["protocol"],
+ ),
+ )
+ for direction in ("source", "destination"):
+ match = rule.get(direction)
+ if not match:
+ continue
+ mbase = rbase + [direction]
+ if match.get("address"):
+ client.configure_set(mbase + ["address"], match["address"])
+ commands.append(
+ "set {p} address '{a}'".format(
+ p=" ".join(mbase),
+ a=match["address"],
+ ),
+ )
+ if match.get("port"):
+ client.configure_set(mbase + ["port"], match["port"])
+ commands.append(
+ "set {p} port {pt}".format(
+ p=" ".join(mbase),
+ pt=match["port"],
+ ),
+ )
+
+ state_match = rule.get("state")
+ if state_match:
+ sbase = rbase + ["state"]
+ for st_name in ("established", "new", "related", "invalid"):
+ if state_match.get(st_name):
+ client.configure_set(sbase + [st_name], "enable")
+ commands.append(
+ "set {p} {s} enable".format(
+ p=" ".join(sbase),
+ s=st_name,
+ ),
+ )
+
+ if "enabled" in rule and not rule["enabled"]:
+ client.configure_set(rbase + ["disable"])
+ commands.append("set {p} disable".format(p=" ".join(rbase)))
+
+
+def main():
+ rule_spec = dict(
+ number=dict(type="int", required=True),
+ action=dict(type="str", choices=["drop", "reject", "accept", "inspect"]),
+ description=dict(type="str"),
+ protocol=dict(type="str"),
+ source=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ group=dict(
+ type="dict",
+ options=dict(
+ address_group=dict(type="str"),
+ network_group=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ destination=dict(
+ type="dict",
+ options=dict(
+ address=dict(type="str"),
+ port=dict(type="str"),
+ group=dict(
+ type="dict",
+ options=dict(
+ address_group=dict(type="str"),
+ network_group=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="dict",
+ options=dict(
+ established=dict(type="bool"),
+ new=dict(type="bool"),
+ related=dict(type="bool"),
+ invalid=dict(type="bool"),
+ ),
+ ),
+ enabled=dict(type="bool", default=True),
+ )
+
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ rule_sets=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ default_action=dict(
+ type="str",
+ choices=["drop", "reject", "accept"],
+ default="drop",
+ ),
+ description=dict(type="str"),
+ rules=dict(type="list", elements="dict", options=rule_spec),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+
+ before = _get_fw_rules(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state in ("deleted", "overridden") and not config:
+ for afi_key in ("name", "ipv6-name"):
+ try:
+ client.configure_delete(["firewall", afi_key])
+ commands.append(
+ "delete firewall {k}".format(k=afi_key),
+ )
+ except VyOSRestError:
+ pass
+ changed = True
+
+ elif state == "deleted" and config:
+ for entry in config:
+ afi = entry["afi"]
+ base_path = _FW_PATH[afi]
+ for rs in entry.get("rule_sets") or []:
+ try:
+ client.configure_delete(base_path + [rs["name"]])
+ commands.append(
+ "delete {p} {n}".format(
+ p=" ".join(base_path),
+ n=rs["name"],
+ ),
+ )
+ changed = True
+ except VyOSRestError:
+ pass
+
+ elif state in ("merged", "replaced", "overridden"):
+ if state == "replaced":
+ for entry in config:
+ afi = entry["afi"]
+ base_path = _FW_PATH[afi]
+ for rs in entry.get("rule_sets") or []:
+ try:
+ client.configure_delete(base_path + [rs["name"]])
+ commands.append(
+ "delete {p} {n}".format(
+ p=" ".join(base_path),
+ n=rs["name"],
+ ),
+ )
+ except VyOSRestError:
+ pass
+ for entry in config:
+ afi = entry["afi"]
+ for rs in entry.get("rule_sets") or []:
+ _apply_rule_set(client, afi, rs, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_fw_rules(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_generate.py b/plugins/modules/vyos_generate.py
new file mode 100644
index 0000000..448575a
--- /dev/null
+++ b/plugins/modules/vyos_generate.py
@@ -0,0 +1,102 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_generate
+short_description: Execute generate commands on a VyOS device via REST API.
+description:
+ - Sends a C(generate) operational command to a VyOS device via the HTTPS
+ REST API (C(/generate) endpoint).
+ - Commonly used to generate WireGuard keypairs, TLS certificates, etc.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ path:
+ description:
+ - Generate command tokens.
+ - E.g. C(["wireguard", "default-keypair"]).
+ type: list
+ elements: str
+ required: true
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+examples: |
+ - name: Generate WireGuard default keypair
+ vyos.rest.vyos_generate:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["wireguard", "default-keypair"]
+
+ - name: Generate a new SSH host key
+ vyos.rest.vyos_generate:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["ssh-server", "host-key", "rsa"]
+"""
+
+RETURN = r"""
+output:
+ description: Text output produced by the generate command (may be empty).
+ returned: success
+ type: str
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ path=dict(type="list", elements="str", required=True),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=False,
+ )
+
+ client = VyOSRestClient(module)
+ try:
+ result = client.generate(module.params["path"])
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=True, output=result.get("data", ""))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_hostname.py b/plugins/modules/vyos_hostname.py
index 89f5cec..10255b5 100644
--- a/plugins/modules/vyos_hostname.py
+++ b/plugins/modules/vyos_hostname.py
@@ -1,310 +1,187 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
-from ansible.module_utils.basic import AnsibleModule
-from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+__metaclass__ = type
DOCUMENTATION = r"""
---
module: vyos_hostname
-short_description: Manage the hostname of a VyOS device using REST API
+short_description: Manage the system hostname on a VyOS device via the REST API.
description:
- - Configures the system hostname on VyOS network devices via the REST API.
- - Supports idempotent configuration — no change is made if the desired
- hostname already matches the running configuration.
- - Uses REST API (C(connection=httpapi)) instead of CLI.
+ - Manages the C(set system host-name) configuration on a VyOS device
+ using the HTTPS REST API.
+ - Mirrors the behaviour of C(vyos.vyos.vyos_hostname) but uses the HTTP
+ API instead of SSH/network_cli.
version_added: "1.0.0"
author:
- - Your Name (@yourhandle)
-
+ - VyOS Community (@vyos)
options:
config:
description:
- - Hostname configuration dictionary.
+ - Hostname configuration.
type: dict
suboptions:
hostname:
description:
- - The hostname to set on the VyOS device.
- - Must be a valid RFC 1123 hostname.
+ - System hostname (max 63 characters, no underscores).
type: str
-
+ required: true
state:
description:
- - The desired state of the hostname configuration.
- - C(merged), C(replaced), and C(overridden) are identical for this
- single-value resource — all three ensure the hostname is set to
- the value specified in C(config).
- - C(deleted) removes the configured hostname, reverting to the
- device default.
- - C(gathered) retrieves the current hostname from the device and
- returns it as structured data in the C(gathered) key. No changes
- are made to the device.
+ - C(merged) - Ensure the hostname is set to the value in I(config).
+ - C(deleted) - Remove the configured hostname (resets to default).
+ - C(gathered) - Read the current hostname from the device and return it
+ in I(gathered) without making changes.
type: str
+ choices: [merged, deleted, gathered]
default: merged
- choices:
- - merged
- - replaced
- - overridden
- - deleted
- - gathered
-
-notes:
- - Tested against VyOS 1.3 (equuleus) and 1.4 (sagitta).
- - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
- - The C(ansible_network_os) inventory variable must be set to C(vyos.rest.vyos).
-"""
-
-EXAMPLES = r"""
-# Before state:
-# -------------
-# vyos@router:~$ show configuration commands | grep host-name
-# set system host-name 'vyostest'
-
-- name: Set hostname using merged state
- vyos.rest.vyos_hostname:
- config:
- hostname: vyos
- state: merged
-
-# After state:
-# ------------
-# set system host-name 'vyos'
-
-# ------------------------------------------------------------------------
-
-# Before state:
-# -------------
-# set system host-name 'vyos'
-
-- name: Override hostname (identical behaviour to merged for this resource)
- vyos.rest.vyos_hostname:
- config:
- hostname: vyosTest
- state: overridden
-
-# After state:
-# ------------
-# set system host-name 'vyosTest'
-
-# ------------------------------------------------------------------------
-
-# Before state:
-# -------------
-# set system host-name 'vyos'
-
-- name: Replace hostname
- vyos.rest.vyos_hostname:
- config:
- hostname: vyosRouter
- state: replaced
-
-# After state:
-# ------------
-# set system host-name 'vyosRouter'
-
-# ------------------------------------------------------------------------
-
-# Before state:
-# -------------
-# set system host-name 'vyos'
-
-- name: Delete configured hostname
- vyos.rest.vyos_hostname:
- state: deleted
-
-# After state:
-# ------------
-# (no host-name entry — device reverts to default)
-
-# ------------------------------------------------------------------------
-
-- name: Gather current hostname from device
- vyos.rest.vyos_hostname:
- state: gathered
-
-# Module result:
-# --------------
-# "gathered": {
-# "hostname": "vyos"
-# }
-
-# ------------------------------------------------------------------------
-
-- name: Idempotency — no change when hostname already matches
- vyos.rest.vyos_hostname:
- config:
- hostname: vyos
- state: merged
-
-# Module result (hostname already 'vyos'):
-# ----------------------------------------
-# "changed": false
+ hostname:
+ description:
+ - IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description:
+ - HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description:
+ - API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description:
+ - Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description:
+ - Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_hostname
"""
RETURN = r"""
before:
- description: Hostname configuration on the device before this module ran.
- returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ description: Configuration on the device before the module ran.
+ returned: always
type: dict
- sample:
- hostname: vyostest
-
after:
- description: Hostname configuration on the device after this module ran.
+ description: Configuration on the device after the module ran.
returned: when changed
type: dict
- sample:
- hostname: vyos
-
-commands:
- description: List of API command dicts sent to the device.
- returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
- type: list
- sample:
- - op: set
- path: ["system", "host-name", "vyos"]
-
gathered:
- description: >
- Current hostname configuration retrieved from the device as structured
- data. Returned only when I(state) is C(gathered).
- returned: when I(state) is C(gathered)
- type: dict
- sample:
- hostname: vyos
-
-response:
- description: Raw response returned by the VyOS REST API.
- returned: when changes are applied
- type: dict
- sample:
- success: true
- data: null
- error: null
-
-saved:
- description: Result of the save_config call issued after applying changes.
- returned: when changes are applied
+ description: Hostname read from the device (state=gathered only).
+ returned: when state is gathered
type: dict
+commands:
+ description: REST API commands dispatched.
+ returned: always
+ type: list
"""
-def get_running_config(vyos):
-
- raw = vyos.get_config(["system", "host-name"])
-
- hostname = None
-
- if isinstance(raw, dict):
- hostname = raw.get("host-name")
-
- elif isinstance(raw, str):
- hostname = raw.strip()
- else:
- hostname = None
-
- return {"hostname": hostname}
-
-
-def build_commands(want, have, state):
+EXAMPLES = r"""
+- name: Set hostname
+ vyos.rest.vyos_hostname:
+ config:
+ hostname: vyos-core-01
+ state: merged
- commands = []
+- name: Gather current hostname
+ vyos.rest.vyos_hostname:
+ state: gathered
+ register: result
- want_host = want.get("hostname")
- have_host = have.get("hostname")
+- name: Delete hostname configuration
+ vyos.rest.vyos_hostname:
+ state: deleted
+"""
- if state in ["merged", "replaced", "overridden"]:
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
- if want_host and want_host != have_host:
- commands.append(
- {
- "op": "set",
- "path": ["system", "host-name", want_host],
- },
- )
- elif state == "deleted":
+_PATH = ["system", "host-name"]
- if have_host:
- commands.append(
- {
- "op": "delete",
- "path": ["system", "host-name"],
- },
- )
- return commands
+def _get_hostname(client):
+ try:
+ result = client.retrieve_return_value(_PATH)
+ return result.get("data", "")
+ except VyOSRestError:
+ return ""
def main():
-
argument_spec = dict(
config=dict(
type="dict",
options=dict(
- hostname=dict(type="str"),
+ hostname=dict(type="str", required=True),
),
),
state=dict(
+ type="str",
default="merged",
- choices=[
- "merged",
- "replaced",
- "overridden",
- "deleted",
- "gathered",
- ],
+ choices=["merged", "deleted", "gathered"],
),
)
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
module = AnsibleModule(
argument_spec=argument_spec,
+ required_if=[("state", "merged", ["config"])],
supports_check_mode=True,
)
- vyos = VyOSModule(module)
-
+ client = VyOSRestClient(module)
state = module.params["state"]
- want = module.params.get("config") or {}
+ commands = []
+ changed = False
- have = get_running_config(vyos)
+ current = _get_hostname(client)
+ before = {"hostname": current}
if state == "gathered":
- module.exit_json(
- changed=False,
- gathered=have,
- )
-
- commands = build_commands(want, have, state)
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
if module.check_mode:
- module.exit_json(
- changed=bool(commands),
- commands=commands,
- )
-
- if commands:
-
- response = vyos.apply_commands(commands)
-
- # save config if change applied
- saved = vyos.save_config()
-
- module.exit_json(
- changed=True,
- before=have,
- after=want,
- commands=commands,
- saved=saved,
- response=response,
- )
-
- module.exit_json(
- changed=False,
- before=have,
- after=have,
- )
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "merged":
+ desired = module.params["config"]["hostname"]
+ if current != desired:
+ client.configure_set(_PATH, desired)
+ commands.append(
+ "set system host-name '{h}'".format(h=desired),
+ )
+ changed = True
+ elif state == "deleted":
+ if current:
+ client.configure_delete(_PATH)
+ commands.append("delete system host-name")
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = {"hostname": _get_hostname(client)} if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
if __name__ == "__main__":
diff --git a/plugins/modules/vyos_image.py b/plugins/modules/vyos_image.py
new file mode 100644
index 0000000..be12705
--- /dev/null
+++ b/plugins/modules/vyos_image.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_image
+short_description: Manage VyOS system images via the REST API.
+description:
+ - Adds or removes VyOS system images using the HTTPS REST API
+ (C(/image) endpoint).
+ - Use I(state=present) to download and install an image from a URL.
+ - Use I(state=absent) to delete an installed image by name.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ state:
+ description:
+ - C(present): Download and install the image from I(url).
+ - C(absent): Delete the image specified by I(name).
+ type: str
+ choices: [present, absent]
+ required: true
+ url:
+ description:
+ - HTTP(S) URL of the VyOS C(.iso) file to install.
+ - Required when I(state=present).
+ type: str
+ name:
+ description:
+ - Version name of the image to remove (e.g. C(1.4-rolling-202401010000)).
+ - Required when I(state=absent).
+ type: str
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: >
+ Request timeout in seconds. Image downloads can take several minutes;
+ increase this value accordingly.
+ type: int
+ default: 300
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+examples: |
+ - name: Install the latest rolling release
+ vyos.rest.vyos_image:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: present
+ url: "https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso"
+ timeout: 600
+
+ - name: Remove an old image
+ vyos.rest.vyos_image:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: absent
+ name: "1.4-rolling-202312010000"
+"""
+
+RETURN = r"""
+output:
+ description: Text output from the image add/delete operation.
+ returned: success
+ type: str
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ state=dict(type="str", required=True, choices=["present", "absent"]),
+ url=dict(type="str"),
+ name=dict(type="str"),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ # Override default timeout to 300s for image downloads
+ argument_spec["timeout"]["default"] = 300
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[
+ ("state", "present", ["url"]),
+ ("state", "absent", ["name"]),
+ ],
+ supports_check_mode=False,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+
+ try:
+ if state == "present":
+ result = client.image_add(module.params["url"])
+ else:
+ result = client.image_delete(module.params["name"])
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=True, output=result.get("data", ""))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_info.py b/plugins/modules/vyos_info.py
new file mode 100644
index 0000000..8946868
--- /dev/null
+++ b/plugins/modules/vyos_info.py
@@ -0,0 +1,115 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_info
+short_description: Retrieve system information from a VyOS device via the REST API.
+description:
+ - Queries the public C(/info) endpoint (HTTP GET) which requires no
+ authentication and returns the VyOS version, hostname, and a welcome banner.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: >
+ API key. Not required for this module (the /info endpoint is public)
+ but kept in the spec for consistency with other modules in this collection.
+ type: str
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 10
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+examples: |
+ - name: Get VyOS system info
+ vyos.rest.vyos_info:
+ hostname: 192.168.1.1
+ register: sys_info
+
+ - name: Print VyOS version
+ ansible.builtin.debug:
+ msg: "Running VyOS {{ sys_info.version }}"
+"""
+
+RETURN = r"""
+version:
+ description: VyOS version string.
+ returned: success
+ type: str
+ sample: "1.4-rolling-202401010000"
+hostname:
+ description: System hostname.
+ returned: success
+ type: str
+ sample: vyos-router
+banner:
+ description: Welcome banner text.
+ returned: success
+ type: str
+ sample: "Welcome to VyOS"
+info:
+ description: Full info dict as returned by the device.
+ returned: success
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict()
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ # api_key not strictly required for /info
+ argument_spec["api_key"]["required"] = False
+ argument_spec["api_key"]["default"] = ""
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ try:
+ result = client.info()
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ data = result.get("data", {})
+ module.exit_json(
+ changed=False,
+ version=data.get("version", ""),
+ hostname=data.get("hostname", ""),
+ banner=data.get("banner", ""),
+ info=data,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py
new file mode 100644
index 0000000..9edc1da
--- /dev/null
+++ b/plugins/modules/vyos_interfaces.py
@@ -0,0 +1,401 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_interfaces
+short_description: Manage interface attributes on a VyOS device via the REST API.
+description:
+ - Manages interface base attributes (description, MTU, enabled state, VIFs,
+ duplex, speed) on VyOS devices using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_interfaces) but uses HTTP API.
+ - Supports Ethernet, Bonding, VXLAN, Loopback, and Virtual Tunnel interfaces.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: >
+ Full interface name (e.g. C(eth0), C(bond0), C(vti1), C(vxlan2)).
+ type: str
+ required: true
+ description:
+ description: Interface description.
+ type: str
+ enabled:
+ description: >
+ Administrative state. C(true) = up, C(false) = admin-down.
+ type: bool
+ default: true
+ mtu:
+ description: MTU in bytes. Applicable for Ethernet, Bonding, VXLAN, VTI.
+ type: int
+ duplex:
+ description: Duplex mode. Only for Ethernet interfaces.
+ type: str
+ choices: [auto, full, half]
+ speed:
+ description: Link speed. Only for Ethernet interfaces.
+ type: str
+ vifs:
+ description: List of VLAN sub-interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ vlan_id:
+ description: 802.1Q VLAN ID.
+ type: int
+ required: true
+ description:
+ description: VIF description.
+ type: str
+ enabled:
+ description: Administrative state of the VIF.
+ type: bool
+ default: true
+ state:
+ description:
+ - C(merged): Merge the provided config with existing interface config.
+ - C(replaced): Replace per-interface config with provided values.
+ - C(overridden): Override all interface config with provided values.
+ - C(deleted): Delete interface config for listed interfaces (or all).
+ - C(gathered): Read current interface config from device.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_interfaces
+ - module: vyos.rest.vyos_l3_interfaces
+examples: |
+ - name: Set description and MTU on eth1
+ vyos.rest.vyos_interfaces:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - name: eth1
+ description: "WAN uplink"
+ mtu: 1500
+ enabled: true
+ state: merged
+
+ - name: Disable eth2
+ vyos.rest.vyos_interfaces:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - name: eth2
+ enabled: false
+ state: merged
+
+ - name: Create a VLAN sub-interface
+ vyos.rest.vyos_interfaces:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - name: eth1
+ vifs:
+ - vlan_id: 100
+ description: "VLAN 100"
+ state: merged
+"""
+
+RETURN = r"""
+before:
+ description: Interface configuration before the module ran.
+ returned: always
+ type: list
+after:
+ description: Interface configuration after the module ran.
+ returned: when changed
+ type: list
+gathered:
+ description: Interface config read from device (state=gathered).
+ returned: when state is gathered
+ type: list
+commands:
+ description: REST API set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_IFACE_TYPES = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "vti": "vti",
+ "vxlan": "vxlan",
+ "lo": "loopback",
+ "dummy": "dummy",
+ "peth": "pseudo-ethernet",
+ "vtun": "openvpn",
+ "wg": "wireguard",
+ "br": "bridge",
+ "macsec": "macsec",
+ "tun": "tunnel",
+}
+
+
+def _iface_type(name):
+ """Infer the VyOS interface type from its name prefix."""
+ for prefix, itype in _IFACE_TYPES.items():
+ if name.startswith(prefix):
+ return itype
+ return "ethernet"
+
+
+def _iface_base_path(name):
+ return ["interfaces", _iface_type(name), name]
+
+
+def _get_interfaces(client):
+ try:
+ result = client.retrieve_show_config(["interfaces"])
+ ifaces_raw = result.get("data") or {}
+ interfaces = []
+ for itype, itype_data in ifaces_raw.items():
+ if not isinstance(itype_data, dict):
+ continue
+ for iname, idata in itype_data.items():
+ if not isinstance(idata, dict):
+ continue
+ entry = {"name": iname}
+ if "description" in idata:
+ entry["description"] = idata["description"]
+ if "mtu" in idata:
+ entry["mtu"] = int(idata["mtu"])
+ entry["enabled"] = "disable" not in idata
+ if "duplex" in idata:
+ entry["duplex"] = idata["duplex"]
+ if "speed" in idata:
+ entry["speed"] = idata["speed"]
+ if "vif" in idata:
+ vifs = []
+ for vid, vdata in idata["vif"].items():
+ vif_entry = {"vlan_id": int(vid)}
+ if isinstance(vdata, dict):
+ if "description" in vdata:
+ vif_entry["description"] = vdata["description"]
+ vif_entry["enabled"] = "disable" not in vdata
+ vifs.append(vif_entry)
+ entry["vifs"] = vifs
+ interfaces.append(entry)
+ return interfaces
+ except VyOSRestError:
+ return []
+
+
+def _apply_interface(client, iface_cfg, commands):
+ name = iface_cfg["name"]
+ base = _iface_base_path(name)
+
+ if iface_cfg.get("description") is not None:
+ client.configure_set(base + ["description"], iface_cfg["description"])
+ commands.append(
+ "set interfaces {t} {n} description '{d}'".format(
+ t=_iface_type(name),
+ n=name,
+ d=iface_cfg["description"],
+ ),
+ )
+ if iface_cfg.get("mtu") is not None:
+ client.configure_set(base + ["mtu"], str(iface_cfg["mtu"]))
+ commands.append(
+ "set interfaces {t} {n} mtu {m}".format(
+ t=_iface_type(name),
+ n=name,
+ m=iface_cfg["mtu"],
+ ),
+ )
+ if "enabled" in iface_cfg:
+ if not iface_cfg["enabled"]:
+ client.configure_set(base + ["disable"])
+ commands.append(
+ "set interfaces {t} {n} disable".format(t=_iface_type(name), n=name),
+ )
+ else:
+ try:
+ client.configure_delete(base + ["disable"])
+ commands.append(
+ "delete interfaces {t} {n} disable".format(
+ t=_iface_type(name),
+ n=name,
+ ),
+ )
+ except VyOSRestError:
+ pass
+ if iface_cfg.get("duplex"):
+ client.configure_set(base + ["duplex"], iface_cfg["duplex"])
+ commands.append(
+ "set interfaces {t} {n} duplex {d}".format(
+ t=_iface_type(name),
+ n=name,
+ d=iface_cfg["duplex"],
+ ),
+ )
+ if iface_cfg.get("speed"):
+ client.configure_set(base + ["speed"], iface_cfg["speed"])
+ commands.append(
+ "set interfaces {t} {n} speed {s}".format(
+ t=_iface_type(name),
+ n=name,
+ s=iface_cfg["speed"],
+ ),
+ )
+ for vif in iface_cfg.get("vifs") or []:
+ vid = str(vif["vlan_id"])
+ vif_base = base + ["vif", vid]
+ client.configure_set(vif_base)
+ commands.append(
+ "set interfaces {t} {n} vif {v}".format(
+ t=_iface_type(name),
+ n=name,
+ v=vid,
+ ),
+ )
+ if vif.get("description"):
+ client.configure_set(vif_base + ["description"], vif["description"])
+ commands.append(
+ "set interfaces {t} {n} vif {v} description '{d}'".format(
+ t=_iface_type(name),
+ n=name,
+ v=vid,
+ d=vif["description"],
+ ),
+ )
+ if "enabled" in vif and not vif["enabled"]:
+ client.configure_set(vif_base + ["disable"])
+ commands.append(
+ "set interfaces {t} {n} vif {v} disable".format(
+ t=_iface_type(name),
+ n=name,
+ v=vid,
+ ),
+ )
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ enabled=dict(type="bool", default=True),
+ mtu=dict(type="int"),
+ duplex=dict(type="str", choices=["auto", "full", "half"]),
+ speed=dict(type="str"),
+ vifs=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ vlan_id=dict(type="int", required=True),
+ description=dict(type="str"),
+ enabled=dict(type="bool", default=True),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+
+ before = _get_interfaces(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ targets = {i["name"] for i in config} if config else {i["name"] for i in before}
+ for iface in before:
+ if iface["name"] in targets:
+ base = _iface_base_path(iface["name"])
+ # Only delete mutable attributes, not the interface itself
+ for attr in ["description", "mtu", "duplex", "speed"]:
+ if attr in iface:
+ try:
+ client.configure_delete(base + [attr])
+ commands.append(
+ "delete interfaces {t} {n} {a}".format(
+ t=_iface_type(iface["name"]),
+ n=iface["name"],
+ a=attr,
+ ),
+ )
+ except VyOSRestError:
+ pass
+ changed = True
+
+ elif state in ("merged", "replaced", "overridden"):
+ for iface_cfg in config:
+ _apply_interface(client, iface_cfg, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_interfaces(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_l3_interfaces.py b/plugins/modules/vyos_l3_interfaces.py
new file mode 100644
index 0000000..f6d4e6c
--- /dev/null
+++ b/plugins/modules/vyos_l3_interfaces.py
@@ -0,0 +1,381 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_l3_interfaces
+short_description: Manage L3 interface attributes on VyOS via the REST API.
+description:
+ - Manages IPv4 and IPv6 addresses on VyOS interfaces via the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_l3_interfaces) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of L3 interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Full interface name (e.g. C(eth0), C(eth1)).
+ type: str
+ required: true
+ ipv4:
+ description: List of IPv4 address assignments.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: IPv4 address in CIDR notation or C(dhcp).
+ type: str
+ required: true
+ ipv6:
+ description: List of IPv6 address assignments.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: IPv6 address in CIDR notation or C(dhcpv6) or C(autoconf).
+ type: str
+ required: true
+ vifs:
+ description: VLAN sub-interface L3 settings.
+ type: list
+ elements: dict
+ suboptions:
+ vlan_id:
+ description: 802.1Q VLAN ID.
+ type: int
+ required: true
+ ipv4:
+ description: IPv4 addresses for this VIF.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ required: true
+ ipv6:
+ description: IPv6 addresses for this VIF.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ required: true
+ state:
+ description:
+ - C(merged): Add addresses to the interface (preserve existing).
+ - C(replaced): Replace all addresses on each listed interface.
+ - C(overridden): Replace all addresses on all interfaces.
+ - C(deleted): Remove all addresses from listed (or all) interfaces.
+ - C(gathered): Read L3 config from device without changes.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_l3_interfaces
+ - module: vyos.rest.vyos_interfaces
+examples: |
+ - name: Assign addresses to eth1 and eth2
+ vyos.rest.vyos_l3_interfaces:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - name: eth1
+ ipv4:
+ - address: 10.0.1.1/24
+ ipv6:
+ - address: "2001:db8::1/64"
+ - name: eth2
+ ipv4:
+ - address: dhcp
+ state: merged
+
+ - name: Remove all addresses from eth1
+ vyos.rest.vyos_l3_interfaces:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - name: eth1
+ state: deleted
+"""
+
+RETURN = r"""
+before:
+ description: L3 interface config before the module ran.
+ returned: always
+ type: list
+after:
+ description: L3 interface config after the module ran.
+ returned: when changed
+ type: list
+gathered:
+ description: L3 config read from device (state=gathered).
+ returned: when state is gathered
+ type: list
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_IFACE_TYPES = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "vti": "vti",
+ "vxlan": "vxlan",
+ "lo": "loopback",
+ "dummy": "dummy",
+ "br": "bridge",
+ "wg": "wireguard",
+ "tun": "tunnel",
+}
+
+
+def _iface_type(name):
+ for prefix, t in _IFACE_TYPES.items():
+ if name.startswith(prefix):
+ return t
+ return "ethernet"
+
+
+def _base(name):
+ return ["interfaces", _iface_type(name), name]
+
+
+def _get_l3_interfaces(client):
+ try:
+ result = client.retrieve_show_config(["interfaces"])
+ raw = result.get("data") or {}
+ out = []
+ for itype, itype_data in raw.items():
+ if not isinstance(itype_data, dict):
+ continue
+ for iname, idata in itype_data.items():
+ if not isinstance(idata, dict):
+ continue
+ entry = {"name": iname, "ipv4": [], "ipv6": []}
+ for addr in _listify(idata.get("address")):
+ if ":" in addr:
+ entry["ipv6"].append({"address": addr})
+ elif addr == "dhcp":
+ entry["ipv4"].append({"address": addr})
+ else:
+ entry["ipv4"].append({"address": addr})
+ # VIFs
+ if "vif" in idata:
+ vifs = []
+ for vid, vdata in idata["vif"].items():
+ vif_e = {"vlan_id": int(vid), "ipv4": [], "ipv6": []}
+ if isinstance(vdata, dict):
+ for addr in _listify(vdata.get("address")):
+ if ":" in addr:
+ vif_e["ipv6"].append({"address": addr})
+ else:
+ vif_e["ipv4"].append({"address": addr})
+ vifs.append(vif_e)
+ entry["vifs"] = vifs
+ if entry["ipv4"] or entry["ipv6"] or entry.get("vifs"):
+ out.append(entry)
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _listify(val):
+ """Return val as a list regardless of whether it's a str or list."""
+ if val is None:
+ return []
+ if isinstance(val, list):
+ return val
+ return [val]
+
+
+def _set_addresses(client, name, ipv4, ipv6, commands):
+ base = _base(name)
+ itype = _iface_type(name)
+ for a in ipv4 or []:
+ client.configure_set(base + ["address"], a["address"])
+ commands.append(
+ "set interfaces {t} {n} address '{a}'".format(
+ t=itype,
+ n=name,
+ a=a["address"],
+ ),
+ )
+ for a in ipv6 or []:
+ client.configure_set(base + ["address"], a["address"])
+ commands.append(
+ "set interfaces {t} {n} address '{a}'".format(
+ t=itype,
+ n=name,
+ a=a["address"],
+ ),
+ )
+
+
+def _delete_addresses(client, name, commands):
+ base = _base(name)
+ itype = _iface_type(name)
+ try:
+ client.configure_delete(base + ["address"])
+ commands.append(
+ "delete interfaces {t} {n} address".format(t=itype, n=name),
+ )
+ except VyOSRestError:
+ pass
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ ipv4=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str", required=True)),
+ ),
+ ipv6=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str", required=True)),
+ ),
+ vifs=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ vlan_id=dict(type="int", required=True),
+ ipv4=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str", required=True)),
+ ),
+ ipv6=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str", required=True)),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+
+ before = _get_l3_interfaces(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ targets = {i["name"] for i in config} if config else {i["name"] for i in before}
+ for iface in before:
+ if iface["name"] in targets:
+ _delete_addresses(client, iface["name"], commands)
+ changed = True
+
+ elif state in ("merged", "replaced", "overridden"):
+ if state in ("replaced", "overridden"):
+ # First remove existing addresses on targeted interfaces
+ targets = {i["name"] for i in config}
+ if state == "overridden":
+ targets = {i["name"] for i in before}
+ for iface in before:
+ if iface["name"] in targets:
+ _delete_addresses(client, iface["name"], commands)
+
+ for iface_cfg in config:
+ name = iface_cfg["name"]
+ _set_addresses(
+ client,
+ name,
+ iface_cfg.get("ipv4"),
+ iface_cfg.get("ipv6"),
+ commands,
+ )
+ for vif in iface_cfg.get("vifs") or []:
+ vid = str(vif["vlan_id"])
+ base = _base(name) + ["vif", vid]
+ itype = _iface_type(name)
+ for a in (vif.get("ipv4") or []) + (vif.get("ipv6") or []):
+ client.configure_set(base + ["address"], a["address"])
+ commands.append(
+ "set interfaces {t} {n} vif {v} address '{a}'".format(
+ t=itype,
+ n=name,
+ v=vid,
+ a=a["address"],
+ ),
+ )
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_l3_interfaces(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_lag_interfaces.py b/plugins/modules/vyos_lag_interfaces.py
new file mode 100644
index 0000000..15fe1f3
--- /dev/null
+++ b/plugins/modules/vyos_lag_interfaces.py
@@ -0,0 +1,228 @@
+#!/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_lag_interfaces
+short_description: LAG/bonding interfaces resource module via REST API.
+description:
+ - Manages Link Aggregation Group (LAG/bond) interfaces on VyOS via REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of LAG interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Bond interface name (e.g. bond0).
+ type: str
+ required: true
+ mode:
+ description: Bonding mode.
+ type: str
+ choices: [802.3ad, active-backup, broadcast, round-robin,
+ transmit-load-balance, adaptive-load-balance, xor-hash]
+ members:
+ description: Member interfaces.
+ type: list
+ elements: dict
+ suboptions:
+ member:
+ description: Interface name.
+ type: str
+ primary:
+ description: Primary interface name.
+ type: str
+ hash_policy:
+ description: Transmit hash policy.
+ type: str
+ choices: [layer2, layer2+3, layer3+4]
+ arp_monitor:
+ description: ARP monitoring settings.
+ type: dict
+ suboptions:
+ interval:
+ description: Monitoring interval in ms.
+ type: int
+ target:
+ description: Target IP addresses.
+ type: list
+ elements: str
+ state:
+ description: Desired state.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_MODE_MAP = {
+ "802.3ad": "802.3ad",
+ "active-backup": "active-backup",
+ "broadcast": "broadcast",
+ "round-robin": "round-robin",
+ "transmit-load-balance": "transmit-load-balance",
+ "adaptive-load-balance": "adaptive-load-balance",
+ "xor-hash": "xor-hash",
+}
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(["interfaces", "bonding"])
+ data = r.get("data") or {}
+ out = []
+ for bname, bdata in data.items():
+ entry = {"name": bname}
+ if isinstance(bdata, dict):
+ if "mode" in bdata:
+ entry["mode"] = bdata["mode"]
+ if "primary" in bdata:
+ entry["primary"] = bdata["primary"]
+ if "hash-policy" in bdata:
+ entry["hash_policy"] = bdata["hash-policy"]
+ members = []
+ for m in (
+ bdata.get("member", {}).keys() if isinstance(bdata.get("member"), dict) else []
+ ):
+ members.append({"member": m})
+ if members:
+ entry["members"] = members
+ out.append(entry)
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply(client, cfg, commands):
+ name = cfg["name"]
+ base = ["interfaces", "bonding", name]
+ client.configure_set(base)
+ commands.append("set interfaces bonding {n}".format(n=name))
+
+ if cfg.get("mode"):
+ client.configure_set(base + ["mode"], cfg["mode"])
+ commands.append("set interfaces bonding {n} mode {m}".format(n=name, m=cfg["mode"]))
+ if cfg.get("primary"):
+ client.configure_set(base + ["primary"], cfg["primary"])
+ if cfg.get("hash_policy"):
+ client.configure_set(base + ["hash-policy"], cfg["hash_policy"])
+ for m in cfg.get("members") or []:
+ client.configure_set(["interfaces", "ethernet", m["member"], "bond-group"], name)
+ commands.append("set interfaces ethernet {m} bond-group {n}".format(m=m["member"], n=name))
+ arp = cfg.get("arp_monitor") or {}
+ if arp.get("interval"):
+ client.configure_set(base + ["arp-monitor", "interval"], str(arp["interval"]))
+ for t in arp.get("target") or []:
+ client.configure_set(base + ["arp-monitor", "target"], t)
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ mode=dict(type="str", choices=list(_MODE_MAP.keys())),
+ members=dict(
+ type="list",
+ elements="dict",
+ options=dict(member=dict(type="str")),
+ ),
+ primary=dict(type="str"),
+ hash_policy=dict(type="str", choices=["layer2", "layer2+3", "layer3+4"]),
+ arp_monitor=dict(
+ type="dict",
+ options=dict(
+ interval=dict(type="int"),
+ target=dict(type="list", elements="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ targets = {i["name"] for i in config} if config else {i["name"] for i in before}
+ for b in before:
+ if b["name"] in targets:
+ client.configure_delete(["interfaces", "bonding", b["name"]])
+ commands.append("delete interfaces bonding {n}".format(n=b["name"]))
+ changed = True
+ elif state in ("merged", "replaced", "overridden"):
+ for cfg in config:
+ _apply(client, cfg, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_lldp_global.py b/plugins/modules/vyos_lldp_global.py
new file mode 100644
index 0000000..5d8ff46
--- /dev/null
+++ b/plugins/modules/vyos_lldp_global.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+DOCUMENTATION = r"""
+---
+module: vyos_lldp_global
+short_description: Manage LLDP global configuration on VyOS via REST API.
+description:
+ - Manage LLDP global configuration on VyOS via REST API.
+version_added: "1.0.0"
+author: VyOS Community (@vyos)
+options:
+ config:
+ type: dict
+ suboptions:
+ enable:
+ type: bool
+ addresses:
+ type: list
+ elements: str
+ snmp:
+ description:
+ - C(enable) or C(disable).
+ type: str
+ legacy_protocols:
+ type: list
+ elements: str
+ choices: [cdp, edp, fdp, sonmp]
+ state:
+ type: str
+ default: merged
+ choices: [merged, replaced, deleted, gathered]
+"""
+EXAMPLES = r"""
+- vyos.rest.vyos_lldp_global:
+ config:
+ enable: true
+ addresses: [192.0.2.17]
+ snmp: enable
+ legacy_protocols: [cdp, fdp]
+ state: merged
+"""
+RETURN = r"""
+before:
+ returned: always
+ type: dict
+after:
+ returned: when changed
+ type: dict
+commands:
+ returned: always
+ type: list
+gathered:
+ returned: when state is gathered
+ type: dict
+"""
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+_BASE = ["service", "lldp"]
+
+
+def _get(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw:
+ return {}
+ result = {}
+ if isinstance(raw, dict):
+ result["enable"] = True
+ if "management-address" in raw:
+ v = raw["management-address"]
+ result["addresses"] = (
+ list(v.keys()) if isinstance(v, dict) else ([v] if isinstance(v, str) else list(v))
+ )
+ if "snmp" in raw:
+ snmp = raw["snmp"]
+ result["snmp"] = list(snmp.keys())[0] if isinstance(snmp, dict) else str(snmp)
+ if "legacy-protocols" in raw:
+ v = raw["legacy-protocols"]
+ result["legacy_protocols"] = (
+ list(v.keys()) if isinstance(v, dict) else ([v] if isinstance(v, str) else list(v))
+ )
+ return result
+
+
+def _build(want, have, state):
+ cmds = []
+ if state in ("replaced", "deleted") and have:
+ cmds.append(("delete", _BASE))
+ if state == "deleted":
+ return cmds
+ if want.get("enable") is not False:
+ cmds.append(("set", _BASE))
+ for addr in want.get("addresses") or []:
+ cmds.append(("set", _BASE + ["management-address", addr]))
+ if want.get("snmp"):
+ cmds.append(("set", _BASE + ["snmp", want["snmp"]]))
+ for p in want.get("legacy_protocols") or []:
+ cmds.append(("set", _BASE + ["legacy-protocols", p]))
+ return cmds
+
+
+def main():
+ module = AnsibleModule(
+ dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ enable=dict(type="bool"),
+ addresses=dict(type="list", elements="str"),
+ snmp=dict(type="str"),
+ legacy_protocols=dict(
+ type="list",
+ elements="str",
+ choices=["cdp", "edp", "fdp", "sonmp"],
+ ),
+ ),
+ ),
+ state=dict(default="merged", choices=["merged", "replaced", "deleted", "gathered"]),
+ ),
+ supports_check_mode=True,
+ )
+ vyos = VyOSModule(module)
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+ have = _get(vyos)
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+ commands = _build(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(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_lldp_interfaces.py b/plugins/modules/vyos_lldp_interfaces.py
new file mode 100644
index 0000000..4f012aa
--- /dev/null
+++ b/plugins/modules/vyos_lldp_interfaces.py
@@ -0,0 +1,227 @@
+#!/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_lldp_interfaces
+short_description: LLDP interfaces resource module via REST API.
+description:
+ - Manages per-interface LLDP settings (enable/disable, location) on VyOS
+ via the HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of LLDP interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Interface name.
+ type: str
+ required: true
+ enable:
+ description: Enable LLDP on this interface (default true).
+ type: bool
+ default: true
+ location:
+ description: LLDP-MED location data.
+ type: dict
+ suboptions:
+ coordinate_based:
+ type: dict
+ suboptions:
+ altitude:
+ type: int
+ datum:
+ type: str
+ choices: [WGS84, NAD83, MLLW]
+ latitude:
+ type: str
+ required: true
+ longitude:
+ type: str
+ required: true
+ elin:
+ description: Emergency ELIN number (10-25 digits).
+ type: str
+ state:
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_IFACE_TYPES = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "lo": "loopback",
+ "dummy": "dummy",
+ "br": "bridge",
+}
+
+
+def _itype(name):
+ for p, t in _IFACE_TYPES.items():
+ if name.startswith(p):
+ return t
+ return "ethernet"
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(["service", "lldp", "interface"])
+ data = r.get("data") or {}
+ out = []
+ for iname, idata in data.items():
+ entry = {"name": iname}
+ if isinstance(idata, dict):
+ entry["enable"] = "disable" not in idata
+ out.append(entry)
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply(client, cfg, commands):
+ name = cfg["name"]
+ base = ["service", "lldp", "interface", name]
+ client.configure_set(base)
+ commands.append("set service lldp interface {n}".format(n=name))
+
+ if "enable" in cfg and not cfg["enable"]:
+ client.configure_set(base + ["disable"])
+ commands.append("set service lldp interface {n} disable".format(n=name))
+
+ loc = cfg.get("location") or {}
+ coord = loc.get("coordinate_based") or {}
+ if coord:
+ lbase = base + ["location", "coordinate-based"]
+ if coord.get("latitude"):
+ client.configure_set(lbase + ["latitude"], coord["latitude"])
+ if coord.get("longitude"):
+ client.configure_set(lbase + ["longitude"], coord["longitude"])
+ if coord.get("altitude"):
+ client.configure_set(lbase + ["altitude"], str(coord["altitude"]))
+ if coord.get("datum"):
+ client.configure_set(lbase + ["datum"], coord["datum"])
+ commands.append(
+ "set service lldp interface {n} location coordinate-based ...".format(n=name),
+ )
+ if loc.get("elin"):
+ client.configure_set(base + ["location", "elin"], loc["elin"])
+ commands.append(
+ "set service lldp interface {n} location elin {e}".format(n=name, e=loc["elin"]),
+ )
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ enable=dict(type="bool", default=True),
+ location=dict(
+ type="dict",
+ options=dict(
+ coordinate_based=dict(
+ type="dict",
+ options=dict(
+ altitude=dict(type="int"),
+ datum=dict(type="str", choices=["WGS84", "NAD83", "MLLW"]),
+ latitude=dict(type="str", required=True),
+ longitude=dict(type="str", required=True),
+ ),
+ ),
+ elin=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ for cfg in config if config else before:
+ name = cfg["name"] if isinstance(cfg, dict) else cfg
+ try:
+ client.configure_delete(["service", "lldp", "interface", name])
+ commands.append("delete service lldp interface {n}".format(n=name))
+ changed = True
+ except VyOSRestError:
+ pass
+ elif state in ("merged", "replaced", "overridden"):
+ for cfg in config:
+ _apply(client, cfg, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_logging_global.py b/plugins/modules/vyos_logging_global.py
new file mode 100644
index 0000000..be240ff
--- /dev/null
+++ b/plugins/modules/vyos_logging_global.py
@@ -0,0 +1,482 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+DOCUMENTATION = r"""
+---
+module: vyos_logging_global
+short_description: Manage syslog configuration on VyOS devices using REST API
+description:
+ - Manages syslog (logging) configuration on VyOS devices via the REST API.
+ - Supports console, file, host, user, and global logging targets with
+ per-target facility and severity configuration.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - Sagar Paul (@KB-perByte)
+
+options:
+ config:
+ description: Logging configuration.
+ type: dict
+ suboptions:
+ console:
+ description: Logging to serial console.
+ type: dict
+ suboptions:
+ facilities:
+ type: list
+ elements: dict
+ suboptions:
+ facility:
+ type: str
+ severity:
+ type: str
+ files:
+ description: Logging to local files.
+ type: list
+ elements: dict
+ suboptions:
+ path:
+ type: str
+ archive:
+ type: dict
+ suboptions:
+ file_num:
+ type: int
+ size:
+ type: int
+ facilities:
+ type: list
+ elements: dict
+ suboptions:
+ facility:
+ type: str
+ severity:
+ type: str
+ global_params:
+ description: Global syslog parameters (maps to C(system syslog global)).
+ type: dict
+ suboptions:
+ archive:
+ type: dict
+ suboptions:
+ file_num:
+ type: int
+ size:
+ type: int
+ facilities:
+ type: list
+ elements: dict
+ suboptions:
+ facility:
+ type: str
+ severity:
+ type: str
+ marker_interval:
+ type: int
+ preserve_fqdn:
+ type: bool
+ hosts:
+ description: Logging to remote syslog hosts.
+ type: list
+ elements: dict
+ suboptions:
+ hostname:
+ type: str
+ port:
+ type: int
+ protocol:
+ type: str
+ facilities:
+ type: list
+ elements: dict
+ suboptions:
+ facility:
+ type: str
+ severity:
+ type: str
+ protocol:
+ description: Per-facility protocol override.
+ type: str
+ users:
+ description: Logging to local user terminals.
+ type: list
+ elements: dict
+ suboptions:
+ username:
+ type: str
+ facilities:
+ type: list
+ elements: dict
+ suboptions:
+ facility:
+ type: str
+ severity:
+ type: str
+
+ running_config:
+ description: Used only with state C(parsed).
+ type: str
+
+ state:
+ description:
+ - Desired state of the logging configuration.
+ type: str
+ default: merged
+ choices:
+ - merged
+ - replaced
+ - overridden
+ - deleted
+ - gathered
+ - rendered
+ - parsed
+"""
+
+EXAMPLES = r"""
+- name: Merge logging configuration
+ vyos.rest.vyos_logging_global:
+ config:
+ console:
+ facilities:
+ - facility: local7
+ severity: err
+ files:
+ - path: logFile
+ archive:
+ file_num: 2
+ facilities:
+ - facility: local6
+ severity: emerg
+ hosts:
+ - hostname: 172.16.0.1
+ port: 223
+ facilities:
+ - facility: local7
+ severity: all
+ - facility: all
+ protocol: udp
+ users:
+ - username: vyos
+ facilities:
+ - facility: local7
+ severity: debug
+ global_params:
+ archive:
+ file_num: 2
+ size: 111
+ facilities:
+ - facility: cron
+ severity: debug
+ marker_interval: 111
+ preserve_fqdn: true
+ state: merged
+
+- name: Delete all logging configuration
+ vyos.rest.vyos_logging_global:
+ state: deleted
+
+- name: Gather current logging configuration
+ vyos.rest.vyos_logging_global:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Logging configuration before this module ran.
+ returned: when state is merged, replaced, overridden or deleted
+ type: dict
+after:
+ description: Logging 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 logging configuration from the device.
+ returned: when state is gathered
+ type: dict
+saved:
+ description: Result of save_config after applying changes.
+ returned: when changes are applied
+ type: dict
+"""
+
+
+# ------------------------------------------------------------
+# Normalization
+# ------------------------------------------------------------
+
+
+def normalize_config(cfg):
+ result = {
+ "console": {"facilities": {}},
+ "global": {"facilities": {}},
+ "hosts": {},
+ "files": {},
+ "users": {},
+ }
+
+ for f in cfg.get("console", {}).get("facilities", []):
+ result["console"]["facilities"][f["facility"]] = f.get("severity")
+
+ gp = cfg.get("global_params", {})
+ for f in gp.get("facilities", []):
+ result["global"]["facilities"][f["facility"]] = f.get("severity")
+ if gp.get("archive"):
+ result["global"]["archive"] = gp["archive"]
+ if gp.get("marker_interval"):
+ result["global"]["marker_interval"] = gp["marker_interval"]
+ if gp.get("preserve_fqdn"):
+ result["global"]["preserve_fqdn"] = True
+
+ for h in cfg.get("hosts", []):
+ host = {"port": h.get("port"), "facilities": {}}
+ for f in h.get("facilities", []):
+ host["facilities"][f["facility"]] = {k: v for k, v in f.items() if k != "facility"}
+ result["hosts"][h["hostname"]] = host
+
+ for f in cfg.get("files", []):
+ facilities = {x["facility"]: x.get("severity") for x in f.get("facilities", [])}
+ result["files"][f["path"]] = {
+ "archive": f.get("archive"),
+ "facilities": facilities,
+ }
+
+ for u in cfg.get("users", []):
+ result["users"][u["username"]] = {
+ "facilities": {f["facility"]: f.get("severity") for f in u.get("facilities", [])},
+ }
+
+ return result
+
+
+def normalize_running(raw):
+ result = {
+ "console": {"facilities": {}},
+ "global": {"facilities": {}},
+ "hosts": {},
+ "files": {},
+ "users": {},
+ }
+
+ if not raw:
+ return result
+
+ for f, data in raw.get("console", {}).get("facility", {}).items():
+ result["console"]["facilities"][f] = data.get("level")
+
+ g = raw.get("global", {})
+ for f, data in g.get("facility", {}).items():
+ result["global"]["facilities"][f] = data.get("level")
+ if "archive" in g:
+ result["global"]["archive"] = g["archive"]
+ if "marker" in g and "interval" in g["marker"]:
+ result["global"]["marker_interval"] = g["marker"]["interval"]
+ if "preserve-fqdn" in g:
+ result["global"]["preserve_fqdn"] = True
+
+ for host, data in raw.get("host", {}).items():
+ h = {"port": data.get("port"), "facilities": {}}
+ for f, fd in data.get("facility", {}).items():
+ h["facilities"][f] = {
+ "severity": fd.get("level"),
+ "protocol": fd.get("protocol"),
+ }
+ result["hosts"][host] = h
+
+ for path, data in raw.get("file", {}).items():
+ facilities = {}
+ for f, fd in data.get("facility", {}).items():
+ facilities[f] = fd.get("level")
+ result["files"][path] = {
+ "archive": data.get("archive"),
+ "facilities": facilities,
+ }
+
+ for user, data in raw.get("user", {}).items():
+ facilities = {}
+ for f, fd in data.get("facility", {}).items():
+ facilities[f] = fd.get("level")
+ result["users"][user] = {"facilities": facilities}
+
+ return result
+
+
+# ------------------------------------------------------------
+# Diff helpers
+# ------------------------------------------------------------
+
+
+def diff_facilities(base, want, have, state):
+ cmds = []
+ want_keys = set(want)
+ have_keys = set(have)
+
+ for f in want_keys:
+ if f not in have_keys or want[f] != have[f]:
+ path = base + ["facility", f]
+ if want[f]:
+ path += ["level", want[f]]
+ cmds.append(("set", path))
+
+ if state in ["replaced", "deleted"]:
+ for f in have_keys - want_keys:
+ cmds.append(("delete", base + ["facility", f]))
+
+ return cmds
+
+
+def diff_map(base, want, have, state):
+ cmds = []
+ w = set(want)
+ h = set(have)
+
+ if state in ["merged", "replaced"]:
+ for k in w - h:
+ cmds.append(("set", base + [k]))
+
+ if state in ["replaced", "deleted"]:
+ for k in h - w:
+ cmds.append(("delete", base + [k]))
+
+ return cmds
+
+
+# ------------------------------------------------------------
+# Build commands
+# ------------------------------------------------------------
+
+
+def build_commands(want, have, state):
+ cmds = []
+
+ if state == "overridden":
+ cmds.append(("delete", ["system", "syslog"]))
+ state = "merged"
+
+ cmds += diff_facilities(
+ ["system", "syslog", "console"],
+ want["console"]["facilities"],
+ have["console"]["facilities"],
+ state,
+ )
+
+ cmds += diff_facilities(
+ ["system", "syslog", "global"],
+ want["global"]["facilities"],
+ have["global"]["facilities"],
+ state,
+ )
+
+ cmds += diff_map(
+ ["system", "syslog", "file"],
+ want["files"],
+ have["files"],
+ state,
+ )
+
+ cmds += diff_map(
+ ["system", "syslog", "host"],
+ want["hosts"],
+ have["hosts"],
+ state,
+ )
+
+ cmds += diff_map(
+ ["system", "syslog", "user"],
+ want["users"],
+ have["users"],
+ state,
+ )
+
+ return cmds
+
+
+# ------------------------------------------------------------
+# Running config
+# ------------------------------------------------------------
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(["system", "syslog"])
+ return normalize_running(raw)
+
+
+# ------------------------------------------------------------
+# Main
+# ------------------------------------------------------------
+
+
+def main():
+ argument_spec = dict(
+ config=dict(type="dict"),
+ running_config=dict(type="str"),
+ state=dict(
+ default="merged",
+ choices=[
+ "merged",
+ "replaced",
+ "overridden",
+ "deleted",
+ "gathered",
+ "rendered",
+ "parsed",
+ ],
+ ),
+ )
+
+ module = AnsibleModule(argument_spec, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or {}
+
+ if state == "gathered":
+ module.exit_json(gathered=get_running_config(vyos))
+
+ want = normalize_config(config)
+ have = get_running_config(vyos)
+
+ if state == "deleted":
+ want = {
+ "console": {"facilities": {}},
+ "global": {"facilities": {}},
+ "hosts": {},
+ "files": {},
+ "users": {},
+ }
+
+ commands = build_commands(want, 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=want,
+ 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_ntp_global.py b/plugins/modules/vyos_ntp_global.py
index a13eba5..185dc02 100644
--- a/plugins/modules/vyos_ntp_global.py
+++ b/plugins/modules/vyos_ntp_global.py
@@ -50,10 +50,6 @@ options:
options:
description:
- Per-server options.
- - C(pool) replaces C(dynamic) in VyOS 1.3+.
- - C(nts) was added in VyOS 1.4.
- - C(ptp) and C(interleave) were added in VyOS 1.5.
- - C(preempt) is only available in VyOS 1.3 and earlier.
type: list
elements: str
choices:
@@ -69,29 +65,12 @@ options:
running_config:
description:
- Used only with state C(parsed).
- - Provide the output of C(show configuration commands | grep ntp)
- as a string. The module parses it into structured data and returns
- the result in the C(parsed) key.
- - No device connection is required for this state.
+ - Provide the output of C(show configuration commands | grep ntp).
type: str
state:
description:
- The desired state of the NTP configuration.
- - C(merged) adds or updates the provided configuration without removing
- existing entries not mentioned in the task.
- - C(replaced) fully replaces the running NTP configuration with the
- provided config, removing entries not present in the task.
- - C(overridden) deletes all existing allow-client, listen-address, and
- server entries then applies the desired config from scratch.
- - C(deleted) removes all NTP allow-client, listen-address, and server
- entries managed by this module.
- - C(gathered) retrieves and returns the current NTP configuration as
- structured data. No changes are made to the device.
- - C(rendered) returns the CLI commands that would be generated for the
- provided config without connecting to the device.
- - C(parsed) parses the CLI output provided via C(running_config) into
- structured data without connecting to the device.
type: str
default: merged
choices:
@@ -102,23 +81,12 @@ options:
- gathered
- rendered
- parsed
-
-notes:
- - Tested against VyOS 1.4 (sagitta) and 1.5.
- - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
- - C(ansible_network_os) must be set to C(vyos.rest.vyos).
- - C(replaced) and C(overridden) differ. C(replaced) performs a surgical diff
- removing only entries not in the task. C(overridden) deletes entire subtrees
- first then re-applies, which is safer when option ordering matters.
- - The C(rendered) and C(parsed) states do not require a device connection.
"""
EXAMPLES = r"""
-# Before state:
-# -------------
-# set service ntp server time1.vyos.net
-# set service ntp server time2.vyos.net
-# set service ntp server time3.vyos.net
+- name: Gather current NTP configuration
+ vyos.rest.vyos_ntp_global:
+ state: gathered
- name: Merge NTP configuration
vyos.rest.vyos_ntp_global:
@@ -133,17 +101,6 @@ EXAMPLES = r"""
- prefer
state: merged
-# After state:
-# ------------
-# set service ntp allow-client address '10.6.6.0/24'
-# set service ntp listen-address '10.1.3.1'
-# set service ntp server 203.0.113.0 prefer
-# set service ntp server time1.vyos.net
-# set service ntp server time2.vyos.net
-# set service ntp server time3.vyos.net
-
-# ------------------------------------------------------------------------
-
- name: Replace NTP configuration
vyos.rest.vyos_ntp_global:
config:
@@ -157,106 +114,40 @@ EXAMPLES = r"""
- prefer
state: replaced
-# ------------------------------------------------------------------------
-
-- name: Override NTP configuration
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.3.3.0/24
- listen_addresses:
- - 10.7.8.1
- servers:
- - server: server1
- options:
- - dynamic
- - prefer
- - server: server2
- options:
- - noselect
- - preempt
- - server: serv
- state: overridden
-
-# ------------------------------------------------------------------------
-
- name: Delete all managed NTP configuration
vyos.rest.vyos_ntp_global:
state: deleted
-
-# ------------------------------------------------------------------------
-
-- name: Gather current NTP configuration
- vyos.rest.vyos_ntp_global:
- state: gathered
-
-# ------------------------------------------------------------------------
-
-- name: Render NTP configuration commands offline
- vyos.rest.vyos_ntp_global:
- config:
- allow_clients:
- - 10.7.7.0/24
- - 10.8.8.0/24
- listen_addresses:
- - 10.7.9.1
- servers:
- - server: server7
- - server: server45
- options:
- - noselect
- - prefer
- - pool
- state: rendered
-
-# ------------------------------------------------------------------------
-
-- name: Parse NTP configuration from CLI output
- vyos.rest.vyos_ntp_global:
- running_config: "{{ lookup('file', './ntp_config.cfg') }}"
- state: parsed
"""
RETURN = r"""
before:
- description: NTP configuration on the device before this module ran.
- returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ description: NTP configuration before this module ran.
+ returned: when state is merged, replaced, overridden or deleted
type: dict
-
after:
description: NTP configuration after this module ran.
returned: when changed
type: dict
-
commands:
- description: List of API command tuples or dicts sent to the device.
+ description: List of API commands sent to the device.
returned: always
type: list
-
gathered:
- description: Current NTP configuration retrieved from the device as structured data.
- returned: when I(state) is C(gathered)
+ description: Current NTP configuration as structured data.
+ returned: when state is gathered
type: dict
-
rendered:
- description: CLI commands generated for the provided configuration (offline, no device needed).
- returned: when I(state) is C(rendered)
+ description: CLI commands generated for the provided config (offline).
+ returned: when state is rendered
type: list
-
parsed:
- description: Structured data parsed from the C(running_config) CLI output.
- returned: when I(state) is C(parsed)
+ description: Structured data parsed from running_config.
+ returned: when state is parsed
type: dict
-
saved:
- description: Result of save_config after applying changes.
+ description: Whether the config was saved after changes.
returned: when changes are applied
type: bool
-
-response:
- description: Raw API response from the VyOS REST API.
- returned: when changes are applied
- type: dict
"""
from ansible.module_utils.basic import AnsibleModule
@@ -264,108 +155,84 @@ from ansible_collections.vyos.rest.plugins.module_utils.utils import normalize_t
from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
-# ------------------------------------------------------------
-# Helpers
-# ------------------------------------------------------------
-
-
def normalize_config(config):
-
result = {
- "allow_clients": sorted(config.get("allow_clients", [])),
- "listen_addresses": sorted(config.get("listen_addresses", [])),
+ "allow_clients": sorted(config.get("allow_clients") or []),
+ "listen_addresses": sorted(config.get("listen_addresses") or []),
"servers": {},
}
-
- for s in config.get("servers", []):
-
+ for s in config.get("servers") or []:
name = s["server"]
-
- result["servers"][name] = sorted(s.get("options", []))
-
+ result["servers"][name] = sorted(s.get("options") or [])
return result
def normalize_servers(value):
result = {}
-
if isinstance(value, dict):
for server, data in value.items():
-
if isinstance(data, dict):
result[server] = sorted(list(data.keys()))
-
elif isinstance(data, list):
result[server] = sorted(data)
-
elif isinstance(data, str):
result[server] = [data]
-
else:
result[server] = []
-
elif isinstance(value, list):
for server in value:
result[server] = []
-
elif isinstance(value, str):
result[value] = []
-
return result
def get_running_config(vyos):
-
raw = vyos.get_config(["service", "ntp"])
-
result = {
"allow_clients": [],
"listen_addresses": [],
"servers": {},
}
-
if not raw:
return result
- allow_raw = raw.get("allow-client", {}).get("address", [])
+ # allow-client: handle both VyOS schemas
+ # 1.4: {"allow-client": {"address": {"10.x.x.x/y": {}}}}
+ # 1.5+: {"allow-client": {"10.x.x.x/y": {}}} (no address subnode)
+ allow_raw_outer = raw.get("allow-client", {})
+ if "address" in allow_raw_outer:
+ allow_raw = allow_raw_outer.get("address", [])
+ else:
+ allow_raw = allow_raw_outer
result["allow_clients"] = sorted(normalize_to_list(allow_raw))
- listen_raw = raw.get("listen-address", [])
- result["listen_addresses"] = sorted(normalize_to_list(listen_raw))
-
- servers_raw = raw.get("server", {})
- result["servers"] = normalize_servers(servers_raw)
-
+ result["listen_addresses"] = sorted(
+ normalize_to_list(raw.get("listen-address", [])),
+ )
+ result["servers"] = normalize_servers(raw.get("server", {}))
return result
def build_commands(desired, existing, state):
-
cmds = []
- # overridden = delete all then merged
if state == "overridden":
-
if existing["allow_clients"]:
- cmds.append(("delete", ["service", "ntp", "allow-clients"]))
-
+ cmds.append(("delete", ["service", "ntp", "allow-client"]))
if existing["listen_addresses"]:
cmds.append(("delete", ["service", "ntp", "listen-address"]))
-
if existing["servers"]:
cmds.append(("delete", ["service", "ntp", "server"]))
-
state = "merged"
cmds += diff_list(
"allow-client",
- "address", # singular
+ "address",
desired["allow_clients"],
existing["allow_clients"],
state,
)
-
- # listen_addresses
cmds += diff_list(
"listen-address",
None,
@@ -373,160 +240,94 @@ def build_commands(desired, existing, state):
existing["listen_addresses"],
state,
)
-
- # servers
- cmds += diff_servers(
- desired["servers"],
- existing["servers"],
- state,
- )
-
+ cmds += diff_servers(desired["servers"], existing["servers"], state)
return cmds
def diff_list(node, subnode, desired, existing, state):
-
cmds = []
-
desired = set(desired)
existing = set(existing)
- if state in ["merged", "replaced"]:
-
+ if state in ("merged", "replaced"):
for v in desired - existing:
-
path = ["service", "ntp", node]
-
if subnode:
path += [subnode, v]
else:
path += [v]
-
cmds.append(("set", path))
- if state in ["replaced", "deleted"]:
-
+ if state in ("replaced", "deleted"):
for v in existing - desired:
-
path = ["service", "ntp", node]
-
if subnode:
path += [subnode, v]
else:
path += [v]
-
cmds.append(("delete", path))
return cmds
def diff_servers(desired, existing, state):
-
cmds = []
-
desired_set = set(desired.keys())
existing_set = set(existing.keys())
- # add/update
- if state in ["merged", "replaced"]:
-
+ if state in ("merged", "replaced"):
for server in desired_set:
-
desired_opts = set(desired[server])
existing_opts = set(existing.get(server, []))
-
- # add server
if server not in existing_set:
cmds.append(("set", ["service", "ntp", "server", server]))
-
- # add options
for opt in desired_opts - existing_opts:
-
- cmds.append(
- ("set", ["service", "ntp", "server", server, opt]),
- )
-
- # remove options (replaced only)
+ cmds.append(("set", ["service", "ntp", "server", server, opt]))
if state == "replaced":
-
for opt in existing_opts - desired_opts:
+ cmds.append(("delete", ["service", "ntp", "server", server, opt]))
- cmds.append(
- ("delete", ["service", "ntp", "server", server, opt]),
- )
-
- # delete
- if state in ["replaced", "deleted"]:
-
+ if state in ("replaced", "deleted"):
for server in existing_set - desired_set:
-
cmds.append(("delete", ["service", "ntp", "server", server]))
return cmds
def parse_running_config(text):
-
- result = {
- "allow_clients": [],
- "listen_addresses": [],
- "servers": {},
- }
-
+ result = {"allow_clients": [], "listen_addresses": [], "servers": {}}
for line in text.splitlines():
-
parts = line.strip().split()
-
if len(parts) < 4:
continue
-
if parts[3] == "allow-clients":
result["allow_clients"].append(parts[-1])
-
elif parts[3] == "listen-address":
result["listen_addresses"].append(parts[-1])
-
elif parts[3] == "server":
-
server = parts[4]
-
if server not in result["servers"]:
result["servers"][server] = []
-
if len(parts) > 5:
result["servers"][server].append(parts[5])
-
return result
def render_commands(config):
-
cmds = []
-
for c in config["allow_clients"]:
- cmds.append(f"set service ntp allow-clients address {c}")
-
+ cmds.append("set service ntp allow-client address {c}".format(c=c))
for la in config["listen_addresses"]:
- cmds.append(f"set service ntp listen-address {la}")
-
+ cmds.append("set service ntp listen-address {la}".format(la=la))
for server, opts in config["servers"].items():
-
if not opts:
- cmds.append(f"set service ntp server {server}")
-
+ cmds.append("set service ntp server {s}".format(s=server))
for opt in opts:
- cmds.append(f"set service ntp server {server} {opt}")
-
+ cmds.append("set service ntp server {s} {o}".format(s=server, o=opt))
return cmds
-# ------------------------------------------------------------
-# Main
-# ------------------------------------------------------------
-
-
def main():
-
argument_spec = dict(
config=dict(
type="dict",
@@ -559,93 +360,35 @@ def main():
)
module = AnsibleModule(argument_spec, supports_check_mode=True)
-
vyos = VyOSModule(module)
state = module.params["state"]
config = module.params.get("config") or {}
- result = {
- "changed": False,
- }
-
- # --------------------------------------------------------
- # parsed
- # --------------------------------------------------------
-
if state == "parsed":
-
- parsed = parse_running_config(module.params["running_config"])
-
- module.exit_json(parsed=parsed)
-
- # --------------------------------------------------------
- # rendered
- # --------------------------------------------------------
+ module.exit_json(parsed=parse_running_config(module.params["running_config"]))
desired = normalize_config(config)
if state == "rendered":
-
module.exit_json(rendered=render_commands(desired))
- # --------------------------------------------------------
- # gathered
- # --------------------------------------------------------
-
existing = get_running_config(vyos)
if state == "gathered":
-
module.exit_json(gathered=existing)
- # --------------------------------------------------------
- # deleted
- # --------------------------------------------------------
-
if state == "deleted":
-
- desired = {
- "allow_clients": [],
- "listen_addresses": [],
- "servers": {},
- }
-
- # --------------------------------------------------------
- # diff engine
- # --------------------------------------------------------
-
- # commands = build_commands(desired, existing, state)
-
- # result["before"] = existing
-
- # result["commands"] = commands
-
- # if commands:
-
- # result["changed"] = True
-
- # if not module.check_mode:
-
- # vyos.apply_commands(commands)
-
- # result["after"] = desired
-
- # module.exit_json(**result)
+ desired = {"allow_clients": [], "listen_addresses": [], "servers": {}}
commands = build_commands(desired, existing, state)
if module.check_mode:
- module.exit_json(
- changed=bool(commands),
- commands=commands,
- before=existing,
- )
+ module.exit_json(changed=bool(commands), commands=commands, before=existing)
if commands:
response = vyos.apply_commands(commands)
saved = vyos.save_config()
-
module.exit_json(
changed=True,
before=existing,
@@ -655,12 +398,7 @@ def main():
response=response,
)
- module.exit_json(
- changed=False,
- before=existing,
- after=existing,
- commands=[],
- )
+ module.exit_json(changed=False, before=existing, after=existing, commands=[])
if __name__ == "__main__":
diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py
new file mode 100644
index 0000000..37876d4
--- /dev/null
+++ b/plugins/modules/vyos_ospf_interfaces.py
@@ -0,0 +1,290 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_ospf_interfaces
+short_description: OSPF Interfaces Resource Module via REST API.
+description:
+ - Manages per-interface OSPF parameters (cost, hello/dead timers, authentication,
+ passive mode, etc.) for both OSPFv2 and OSPFv3 via the VyOS HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of OSPF interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Interface name.
+ type: str
+ address_family:
+ description: OSPF settings per address family.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family (ipv4=OSPFv2, ipv6=OSPFv3).
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ authentication:
+ type: dict
+ suboptions:
+ plaintext_password:
+ type: str
+ md5_key:
+ type: dict
+ suboptions:
+ key_id:
+ type: int
+ key:
+ type: str
+ bandwidth:
+ type: int
+ cost:
+ type: int
+ dead_interval:
+ type: int
+ hello_interval:
+ type: int
+ mtu_ignore:
+ type: bool
+ network:
+ type: str
+ priority:
+ type: int
+ retransmit_interval:
+ type: int
+ transmit_delay:
+ type: int
+ ifmtu:
+ type: int
+ instance:
+ type: str
+ passive:
+ type: bool
+ state:
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_IFACE_TYPES = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "lo": "loopback",
+ "dummy": "dummy",
+ "br": "bridge",
+ "wg": "wireguard",
+ "tun": "tunnel",
+ "vxlan": "vxlan",
+}
+
+
+def _itype(name):
+ for p, t in _IFACE_TYPES.items():
+ if name.startswith(p):
+ return t
+ return "ethernet"
+
+
+def _ospf_iface_base(name, afi):
+ itype = _itype(name)
+ if afi == "ipv4":
+ return ["interfaces", itype, name, "ip", "ospf"]
+ return ["interfaces", itype, name, "ipv6", "ospfv3"]
+
+
+def _apply(client, cfg, commands):
+ name = cfg["name"]
+ for af in cfg.get("address_family") or []:
+ afi = af["afi"]
+ base = _ospf_iface_base(name, afi)
+
+ int_map = {
+ "cost": "cost",
+ "bandwidth": "bandwidth",
+ "dead_interval": "dead-interval",
+ "hello_interval": "hello-interval",
+ "priority": "priority",
+ "retransmit_interval": "retransmit-interval",
+ "transmit_delay": "transmit-delay",
+ "ifmtu": "ifmtu",
+ }
+ for param, key in int_map.items():
+ if af.get(param) is not None:
+ client.configure_set(base + [key], str(af[param]))
+ commands.append("set {b} {k} {v}".format(b=" ".join(base), k=key, v=af[param]))
+
+ if af.get("mtu_ignore"):
+ client.configure_set(base + ["mtu-ignore"])
+ commands.append("set {b} mtu-ignore".format(b=" ".join(base)))
+ if af.get("passive"):
+ client.configure_set(base + ["passive"])
+ commands.append("set {b} passive".format(b=" ".join(base)))
+ if af.get("network"):
+ client.configure_set(base + ["network"], af["network"])
+ if af.get("instance"):
+ client.configure_set(base + ["instance-id"], af["instance"])
+
+ auth = af.get("authentication") or {}
+ if auth.get("plaintext_password"):
+ client.configure_set(
+ base + ["authentication", "plaintext-password"],
+ auth["plaintext_password"],
+ )
+ commands.append(
+ "set {b} authentication plaintext-password ...".format(b=" ".join(base)),
+ )
+ md5 = auth.get("md5_key") or {}
+ if md5.get("key_id") and md5.get("key"):
+ client.configure_set(
+ base + ["authentication", "md5", "key-id", str(md5["key_id"]), "md5-key"],
+ md5["key"],
+ )
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(["interfaces"])
+ raw = r.get("data") or {}
+ out = []
+ for itype, itype_data in raw.items():
+ if not isinstance(itype_data, dict):
+ continue
+ for iname, idata in itype_data.items():
+ if not isinstance(idata, dict):
+ continue
+ afs = []
+ ip_ospf = (idata.get("ip") or {}).get("ospf")
+ if ip_ospf:
+ afs.append({"afi": "ipv4", "ospf_data": ip_ospf})
+ ip6_ospfv3 = (idata.get("ipv6") or {}).get("ospfv3")
+ if ip6_ospfv3:
+ afs.append({"afi": "ipv6", "ospfv3_data": ip6_ospfv3})
+ if afs:
+ out.append({"name": iname, "address_family": afs})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str"),
+ address_family=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ authentication=dict(
+ type="dict",
+ options=dict(
+ plaintext_password=dict(type="str", no_log=True),
+ md5_key=dict(
+ type="dict",
+ options=dict(
+ key_id=dict(type="int"),
+ key=dict(type="str", no_log=True),
+ ),
+ ),
+ ),
+ ),
+ bandwidth=dict(type="int"),
+ cost=dict(type="int"),
+ dead_interval=dict(type="int"),
+ hello_interval=dict(type="int"),
+ mtu_ignore=dict(type="bool"),
+ network=dict(type="str"),
+ priority=dict(type="int"),
+ retransmit_interval=dict(type="int"),
+ transmit_delay=dict(type="int"),
+ ifmtu=dict(type="int"),
+ instance=dict(type="str"),
+ passive=dict(type="bool"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ for cfg in config:
+ _apply(client, cfg, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_ospfv2.py b/plugins/modules/vyos_ospfv2.py
new file mode 100644
index 0000000..063d0dc
--- /dev/null
+++ b/plugins/modules/vyos_ospfv2.py
@@ -0,0 +1,487 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_ospfv2
+short_description: OSPFv2 resource module via REST API.
+description:
+ - Manages OSPFv2 (OSPF for IPv4) configuration on VyOS via HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: OSPFv2 configuration.
+ type: dict
+ suboptions:
+ areas:
+ type: list
+ elements: dict
+ suboptions:
+ area_id:
+ type: str
+ area_type:
+ type: dict
+ suboptions:
+ normal:
+ type: bool
+ nssa:
+ type: dict
+ suboptions:
+ set:
+ type: bool
+ default_cost:
+ type: int
+ no_summary:
+ type: bool
+ translate:
+ type: str
+ choices: [always, candidate, never]
+ stub:
+ type: dict
+ suboptions:
+ set:
+ type: bool
+ default_cost:
+ type: int
+ no_summary:
+ type: bool
+ authentication:
+ type: str
+ choices: [plaintext-password, md5]
+ network:
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ range:
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ cost:
+ type: int
+ not_advertise:
+ type: bool
+ substitute:
+ type: str
+ shortcut:
+ type: str
+ choices: [default, disable, enable]
+ auto_cost:
+ type: dict
+ suboptions:
+ reference_bandwidth:
+ type: int
+ default_information:
+ type: dict
+ suboptions:
+ originate:
+ type: dict
+ suboptions:
+ always:
+ type: bool
+ metric:
+ type: int
+ metric_type:
+ type: int
+ route_map:
+ type: str
+ log_adjacency_changes:
+ type: str
+ choices: [detail]
+ max_metric:
+ type: dict
+ suboptions:
+ router_lsa:
+ type: dict
+ suboptions:
+ administrative:
+ type: bool
+ on_shutdown:
+ type: int
+ on_startup:
+ type: int
+ mpls_te:
+ type: dict
+ suboptions:
+ enabled:
+ type: bool
+ router_address:
+ type: str
+ neighbor:
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_id:
+ type: str
+ poll_interval:
+ type: int
+ priority:
+ type: int
+ parameters:
+ type: dict
+ suboptions:
+ router_id:
+ type: str
+ opaque_lsa:
+ type: bool
+ rfc1583_compatibility:
+ type: bool
+ abr_type:
+ type: str
+ choices: [cisco, ibm, shortcut, standard]
+ passive_interface:
+ type: list
+ elements: str
+ redistribute:
+ type: list
+ elements: dict
+ suboptions:
+ route_type:
+ type: str
+ choices: [bgp, connected, kernel, rip, static]
+ metric:
+ type: int
+ metric_type:
+ type: int
+ route_map:
+ type: str
+ state:
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: dict
+after:
+ returned: when changed
+ type: dict
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_BASE = ["protocols", "ospf"]
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(_BASE)
+ return r.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def _apply(client, config, commands):
+ params = config.get("parameters") or {}
+ if params.get("router_id"):
+ client.configure_set(_BASE + ["parameters", "router-id"], params["router_id"])
+ commands.append("set protocols ospf parameters router-id {r}".format(r=params["router_id"]))
+ if params.get("opaque_lsa"):
+ client.configure_set(_BASE + ["parameters", "opaque-lsa"])
+ if params.get("rfc1583_compatibility"):
+ client.configure_set(_BASE + ["parameters", "rfc1583-compatibility"])
+ if params.get("abr_type"):
+ client.configure_set(_BASE + ["parameters", "abr-type"], params["abr_type"])
+
+ ac = config.get("auto_cost") or {}
+ if ac.get("reference_bandwidth"):
+ client.configure_set(
+ _BASE + ["auto-cost", "reference-bandwidth"],
+ str(ac["reference_bandwidth"]),
+ )
+ commands.append(
+ "set protocols ospf auto-cost reference-bandwidth {b}".format(
+ b=ac["reference_bandwidth"],
+ ),
+ )
+
+ if config.get("log_adjacency_changes"):
+ client.configure_set(_BASE + ["log-adjacency-changes", config["log_adjacency_changes"]])
+
+ for nbr in config.get("neighbor") or []:
+ nb = _BASE + ["neighbor", nbr["neighbor_id"]]
+ client.configure_set(nb)
+ if nbr.get("poll_interval"):
+ client.configure_set(nb + ["poll-interval"], str(nbr["poll_interval"]))
+ if nbr.get("priority"):
+ client.configure_set(nb + ["priority"], str(nbr["priority"]))
+
+ for pi in config.get("passive_interface") or []:
+ client.configure_set(_BASE + ["passive-interface"], pi)
+ commands.append("set protocols ospf passive-interface {i}".format(i=pi))
+
+ for redist in config.get("redistribute") or []:
+ rb = _BASE + ["redistribute", redist["route_type"]]
+ client.configure_set(rb)
+ commands.append("set protocols ospf redistribute {r}".format(r=redist["route_type"]))
+ if redist.get("metric"):
+ client.configure_set(rb + ["metric"], str(redist["metric"]))
+ if redist.get("metric_type"):
+ client.configure_set(rb + ["metric-type"], str(redist["metric_type"]))
+ if redist.get("route_map"):
+ client.configure_set(rb + ["route-map"], redist["route_map"])
+
+ di = (config.get("default_information") or {}).get("originate") or {}
+ if di:
+ dib = _BASE + ["default-information", "originate"]
+ client.configure_set(dib)
+ commands.append("set protocols ospf default-information originate")
+ if di.get("always"):
+ client.configure_set(dib + ["always"])
+ if di.get("metric"):
+ client.configure_set(dib + ["metric"], str(di["metric"]))
+ if di.get("metric_type"):
+ client.configure_set(dib + ["metric-type"], str(di["metric_type"]))
+ if di.get("route_map"):
+ client.configure_set(dib + ["route-map"], di["route_map"])
+
+ for area in config.get("areas") or []:
+ aid = area["area_id"]
+ ab = _BASE + ["area", aid]
+ client.configure_set(ab)
+ commands.append("set protocols ospf area {a}".format(a=aid))
+
+ at = area.get("area_type") or {}
+ if at.get("normal"):
+ pass # default
+ nssa = at.get("nssa") or {}
+ if nssa.get("set"):
+ client.configure_set(ab + ["area-type", "nssa"])
+ if nssa.get("default_cost"):
+ client.configure_set(
+ ab + ["area-type", "nssa", "default-cost"],
+ str(nssa["default_cost"]),
+ )
+ if nssa.get("no_summary"):
+ client.configure_set(ab + ["area-type", "nssa", "no-summary"])
+ stub = at.get("stub") or {}
+ if stub.get("set"):
+ client.configure_set(ab + ["area-type", "stub"])
+ if stub.get("default_cost"):
+ client.configure_set(
+ ab + ["area-type", "stub", "default-cost"],
+ str(stub["default_cost"]),
+ )
+
+ if area.get("authentication"):
+ client.configure_set(ab + ["authentication"], area["authentication"])
+
+ for net in area.get("network") or []:
+ client.configure_set(ab + ["network"], net["address"])
+ commands.append(
+ "set protocols ospf area {a} network {n}".format(
+ a=aid,
+ n=net["address"],
+ ),
+ )
+
+ for rng in area.get("range") or []:
+ rb2 = ab + ["range", rng["address"]]
+ client.configure_set(rb2)
+ if rng.get("cost"):
+ client.configure_set(rb2 + ["cost"], str(rng["cost"]))
+ if rng.get("not_advertise"):
+ client.configure_set(rb2 + ["not-advertise"])
+ if rng.get("substitute"):
+ client.configure_set(rb2 + ["substitute"], rng["substitute"])
+
+ if area.get("shortcut"):
+ client.configure_set(ab + ["shortcut"], area["shortcut"])
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ areas=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ area_id=dict(type="str"),
+ area_type=dict(
+ type="dict",
+ options=dict(
+ normal=dict(type="bool"),
+ nssa=dict(
+ type="dict",
+ options=dict(
+ set=dict(type="bool"),
+ default_cost=dict(type="int"),
+ no_summary=dict(type="bool"),
+ translate=dict(
+ type="str",
+ choices=["always", "candidate", "never"],
+ ),
+ ),
+ ),
+ stub=dict(
+ type="dict",
+ options=dict(
+ set=dict(type="bool"),
+ default_cost=dict(type="int"),
+ no_summary=dict(type="bool"),
+ ),
+ ),
+ ),
+ ),
+ authentication=dict(type="str", choices=["plaintext-password", "md5"]),
+ network=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ range=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str"),
+ cost=dict(type="int"),
+ not_advertise=dict(type="bool"),
+ substitute=dict(type="str"),
+ ),
+ ),
+ shortcut=dict(type="str", choices=["default", "disable", "enable"]),
+ ),
+ ),
+ auto_cost=dict(type="dict", options=dict(reference_bandwidth=dict(type="int"))),
+ default_information=dict(
+ type="dict",
+ options=dict(
+ originate=dict(
+ type="dict",
+ options=dict(
+ always=dict(type="bool"),
+ metric=dict(type="int"),
+ metric_type=dict(type="int"),
+ route_map=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ log_adjacency_changes=dict(type="str", choices=["detail"]),
+ max_metric=dict(type="dict"),
+ mpls_te=dict(
+ type="dict",
+ options=dict(
+ enabled=dict(type="bool"),
+ router_address=dict(type="str"),
+ ),
+ ),
+ neighbor=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_id=dict(type="str"),
+ poll_interval=dict(type="int"),
+ priority=dict(type="int"),
+ ),
+ ),
+ parameters=dict(
+ type="dict",
+ options=dict(
+ router_id=dict(type="str"),
+ opaque_lsa=dict(type="bool"),
+ rfc1583_compatibility=dict(type="bool"),
+ abr_type=dict(type="str", choices=["cisco", "ibm", "shortcut", "standard"]),
+ ),
+ ),
+ passive_interface=dict(type="list", elements="str"),
+ redistribute=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ route_type=dict(
+ type="str",
+ choices=["bgp", "connected", "kernel", "rip", "static"],
+ ),
+ metric=dict(type="int"),
+ metric_type=dict(type="int"),
+ route_map=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])],
+ supports_check_mode=True,
+ )
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config")
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ if before:
+ client.configure_delete(_BASE)
+ commands.append("delete protocols ospf")
+ changed = True
+ elif state in ("merged", "replaced"):
+ if state == "replaced" and before:
+ client.configure_delete(_BASE)
+ commands.append("delete protocols ospf")
+ _apply(client, config, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_ospfv3.py b/plugins/modules/vyos_ospfv3.py
new file mode 100644
index 0000000..0de49d7
--- /dev/null
+++ b/plugins/modules/vyos_ospfv3.py
@@ -0,0 +1,257 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_ospfv3
+short_description: OSPFv3 resource module via REST API.
+description:
+ - Manages OSPFv3 (OSPF for IPv6) configuration on VyOS via HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: OSPFv3 configuration.
+ type: dict
+ suboptions:
+ areas:
+ type: list
+ elements: dict
+ suboptions:
+ area_id:
+ type: str
+ export_list:
+ type: str
+ import_list:
+ type: str
+ interface:
+ description: Interfaces in this area.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ range:
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ type: str
+ advertise:
+ type: bool
+ not_advertise:
+ type: bool
+ parameters:
+ type: dict
+ suboptions:
+ router_id:
+ type: str
+ redistribute:
+ type: list
+ elements: dict
+ suboptions:
+ route_type:
+ type: str
+ choices: [bgp, connected, kernel, ripng, static]
+ route_map:
+ type: str
+ state:
+ type: str
+ choices: [merged, replaced, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: dict
+after:
+ returned: when changed
+ type: dict
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_BASE = ["protocols", "ospfv3"]
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(_BASE)
+ return r.get("data") or {}
+ except VyOSRestError:
+ return {}
+
+
+def _apply(client, config, commands):
+ params = config.get("parameters") or {}
+ if params.get("router_id"):
+ client.configure_set(_BASE + ["parameters", "router-id"], params["router_id"])
+ commands.append(
+ "set protocols ospfv3 parameters router-id {r}".format(r=params["router_id"]),
+ )
+
+ for redist in config.get("redistribute") or []:
+ rb = _BASE + ["redistribute", redist["route_type"]]
+ client.configure_set(rb)
+ commands.append("set protocols ospfv3 redistribute {r}".format(r=redist["route_type"]))
+ if redist.get("route_map"):
+ client.configure_set(rb + ["route-map"], redist["route_map"])
+
+ for area in config.get("areas") or []:
+ aid = area["area_id"]
+ ab = _BASE + ["area", aid]
+ client.configure_set(ab)
+ commands.append("set protocols ospfv3 area '{a}'".format(a=aid))
+ if area.get("export_list"):
+ client.configure_set(ab + ["export-list"], area["export_list"])
+ commands.append(
+ "set protocols ospfv3 area {a} export-list {e}".format(
+ a=aid,
+ e=area["export_list"],
+ ),
+ )
+ if area.get("import_list"):
+ client.configure_set(ab + ["import-list"], area["import_list"])
+ for iface in area.get("interface") or []:
+ client.configure_set(ab + ["interface"], iface["name"])
+ commands.append(
+ "set protocols ospfv3 area {a} interface {i}".format(
+ a=aid,
+ i=iface["name"],
+ ),
+ )
+ for rng in area.get("range") or []:
+ rb2 = ab + ["range", rng["address"]]
+ client.configure_set(rb2)
+ commands.append(
+ "set protocols ospfv3 area {a} range {r}".format(
+ a=aid,
+ r=rng["address"],
+ ),
+ )
+ if rng.get("advertise"):
+ client.configure_set(rb2 + ["advertise"])
+ if rng.get("not_advertise"):
+ client.configure_set(rb2 + ["not-advertise"])
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ areas=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ area_id=dict(type="str"),
+ export_list=dict(type="str"),
+ import_list=dict(type="str"),
+ interface=dict(
+ type="list",
+ elements="dict",
+ options=dict(name=dict(type="str")),
+ ),
+ range=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str"),
+ advertise=dict(type="bool"),
+ not_advertise=dict(type="bool"),
+ ),
+ ),
+ ),
+ ),
+ parameters=dict(type="dict", options=dict(router_id=dict(type="str"))),
+ redistribute=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ route_type=dict(
+ type="str",
+ choices=["bgp", "connected", "kernel", "ripng", "static"],
+ ),
+ route_map=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_if=[("state", "merged", ["config"]), ("state", "replaced", ["config"])],
+ supports_check_mode=True,
+ )
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config")
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted":
+ if before:
+ client.configure_delete(_BASE)
+ commands.append("delete protocols ospfv3")
+ changed = True
+ elif state in ("merged", "replaced"):
+ if state == "replaced" and before:
+ client.configure_delete(_BASE)
+ commands.append("delete protocols ospfv3")
+ _apply(client, config, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_ping.py b/plugins/modules/vyos_ping.py
new file mode 100644
index 0000000..45368d1
--- /dev/null
+++ b/plugins/modules/vyos_ping.py
@@ -0,0 +1,180 @@
+#!/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_ping
+short_description: Test reachability using ping via VyOS REST API.
+description:
+ - Sends an ICMP ping via the VyOS HTTPS REST API (/show endpoint) and
+ checks reachability. Mirrors C(vyos.vyos.vyos_ping).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ dest:
+ description: Destination IP address or hostname.
+ type: str
+ required: true
+ count:
+ description: Number of packets to send.
+ type: int
+ default: 5
+ source:
+ description: Source interface or IP address.
+ type: str
+ ttl:
+ description: Time-to-live value.
+ type: int
+ size:
+ description: Packet size in bytes.
+ type: int
+ interval:
+ description: Interval between pings in seconds.
+ type: int
+ state:
+ description: C(present) = expect success; C(absent) = expect failure.
+ type: str
+ choices: [present, absent]
+ default: present
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+commands:
+ description: Ping command tokens sent to the API.
+ returned: always
+ type: list
+packet_loss:
+ description: Packet loss percentage string.
+ returned: always
+ type: str
+packets_rx:
+ description: Packets received.
+ returned: always
+ type: int
+packets_tx:
+ description: Packets transmitted.
+ returned: always
+ type: int
+rtt:
+ description: RTT statistics dict (min/avg/max/mdev).
+ returned: when available
+ type: dict
+"""
+
+import re
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def _build_ping_path(params):
+ path = ["ping", params["dest"]]
+ if params.get("count"):
+ path += ["count", str(params["count"])]
+ if params.get("source"):
+ path += ["interface", params["source"]]
+ if params.get("ttl"):
+ path += ["ttl", str(params["ttl"])]
+ if params.get("size"):
+ path += ["size", str(params["size"])]
+ return path
+
+
+def _parse_ping_output(output):
+ stats = {"packet_loss": "100%", "packets_rx": 0, "packets_tx": 0}
+ tx_match = re.search(r"(\d+) packets transmitted", output)
+ rx_match = re.search(r"(\d+) received", output)
+ loss_match = re.search(r"(\d+(?:\.\d+)?%)\s+packet loss", output)
+ if tx_match:
+ stats["packets_tx"] = int(tx_match.group(1))
+ if rx_match:
+ stats["packets_rx"] = int(rx_match.group(1))
+ if loss_match:
+ stats["packet_loss"] = loss_match.group(1)
+ rtt_match = re.search(
+ r"rtt min/avg/max/mdev = ([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+)",
+ output,
+ )
+ if rtt_match:
+ stats["rtt"] = {
+ "min": rtt_match.group(1),
+ "avg": rtt_match.group(2),
+ "max": rtt_match.group(3),
+ "mdev": rtt_match.group(4),
+ }
+ return stats
+
+
+def main():
+ argument_spec = dict(
+ dest=dict(type="str", required=True),
+ count=dict(type="int", default=5),
+ source=dict(type="str"),
+ ttl=dict(type="int"),
+ size=dict(type="int"),
+ interval=dict(type="int"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+
+ if module.check_mode:
+ module.exit_json(changed=False, commands=[], packet_loss="0%", packets_rx=0, packets_tx=0)
+
+ client = VyOSRestClient(module)
+ path = _build_ping_path(module.params)
+
+ try:
+ result = client.show(path)
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ output = result.get("data", "")
+ stats = _parse_ping_output(output)
+ reachable = stats["packets_rx"] > 0
+
+ if module.params["state"] == "present" and not reachable:
+ module.fail_json(
+ msg="Ping to {dest} failed — no packets received.".format(dest=module.params["dest"]),
+ **stats,
+ )
+ elif module.params["state"] == "absent" and reachable:
+ module.fail_json(
+ msg="Expected {dest} to be unreachable but ping succeeded.".format(
+ dest=module.params["dest"],
+ ),
+ **stats,
+ )
+
+ module.exit_json(changed=False, commands=path, **stats)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_prefix_lists.py b/plugins/modules/vyos_prefix_lists.py
new file mode 100644
index 0000000..63fa584
--- /dev/null
+++ b/plugins/modules/vyos_prefix_lists.py
@@ -0,0 +1,267 @@
+#!/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_prefix_lists
+short_description: Prefix-Lists resource module via REST API.
+description:
+ - Manages IPv4 and IPv6 prefix lists on VyOS via the HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of prefix-list configurations.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ prefix_lists:
+ description: Named prefix lists.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Prefix list name.
+ type: str
+ required: true
+ description:
+ description: Description.
+ type: str
+ entries:
+ description: Prefix list rules.
+ type: list
+ elements: dict
+ suboptions:
+ sequence:
+ description: Rule sequence number.
+ type: int
+ required: true
+ description:
+ type: str
+ action:
+ description: permit or deny.
+ type: str
+ choices: [permit, deny]
+ ge:
+ description: Minimum prefix length.
+ type: int
+ le:
+ description: Maximum prefix length.
+ type: int
+ prefix:
+ description: Network prefix to match.
+ type: str
+ state:
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_PL_KEY = {"ipv4": "prefix-list", "ipv6": "prefix-list6"}
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(["policy"])
+ data = r.get("data") or {}
+ out = []
+ for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]:
+ pl_data = data.get(key) or {}
+ if not isinstance(pl_data, dict):
+ continue
+ pls = []
+ for pl_name, pl_info in pl_data.items():
+ entry = {"name": pl_name}
+ if isinstance(pl_info, dict):
+ if "description" in pl_info:
+ entry["description"] = pl_info["description"]
+ rules = []
+ for seq, rdata in (pl_info.get("rule") or {}).items():
+ if isinstance(rdata, dict):
+ rules.append({"sequence": int(seq), **rdata})
+ if rules:
+ entry["entries"] = rules
+ pls.append(entry)
+ if pls:
+ out.append({"afi": afi, "prefix_lists": pls})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply(client, entry, commands):
+ afi = entry["afi"]
+ key = _PL_KEY[afi]
+ for pl in entry.get("prefix_lists") or []:
+ base = ["policy", key, pl["name"]]
+ client.configure_set(base)
+ commands.append("set policy {k} {n}".format(k=key, n=pl["name"]))
+ if pl.get("description"):
+ client.configure_set(base + ["description"], pl["description"])
+ for rule in pl.get("entries") or []:
+ rb = base + ["rule", str(rule["sequence"])]
+ client.configure_set(rb)
+ commands.append(
+ "set policy {k} {n} rule {s}".format(
+ k=key,
+ n=pl["name"],
+ s=rule["sequence"],
+ ),
+ )
+ if rule.get("action"):
+ client.configure_set(rb + ["action"], rule["action"])
+ commands.append(
+ "set policy {k} {n} rule {s} action {a}".format(
+ k=key,
+ n=pl["name"],
+ s=rule["sequence"],
+ a=rule["action"],
+ ),
+ )
+ if rule.get("prefix"):
+ client.configure_set(rb + ["prefix"], rule["prefix"])
+ if rule.get("ge") is not None:
+ client.configure_set(rb + ["ge"], str(rule["ge"]))
+ if rule.get("le") is not None:
+ client.configure_set(rb + ["le"], str(rule["le"]))
+ if rule.get("description"):
+ client.configure_set(rb + ["description"], rule["description"])
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ prefix_lists=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ entries=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ sequence=dict(type="int", required=True),
+ description=dict(type="str"),
+ action=dict(type="str", choices=["permit", "deny"]),
+ ge=dict(type="int"),
+ le=dict(type="int"),
+ prefix=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted" and not config:
+ for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]:
+ try:
+ client.configure_delete(["policy", key])
+ commands.append("delete policy {k}".format(k=key))
+ except VyOSRestError:
+ pass
+ changed = True
+ elif state == "deleted" and config:
+ for entry in config:
+ key = _PL_KEY[entry["afi"]]
+ for pl in entry.get("prefix_lists") or []:
+ try:
+ client.configure_delete(["policy", key, pl["name"]])
+ commands.append("delete policy {k} {n}".format(k=key, n=pl["name"]))
+ changed = True
+ except VyOSRestError:
+ pass
+ elif state in ("merged", "replaced", "overridden"):
+ if state in ("replaced", "overridden"):
+ for entry in config:
+ key = _PL_KEY[entry["afi"]]
+ for pl in entry.get("prefix_lists") or []:
+ try:
+ client.configure_delete(["policy", key, pl["name"]])
+ except VyOSRestError:
+ pass
+ for entry in config:
+ _apply(client, entry, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_reset.py b/plugins/modules/vyos_reset.py
new file mode 100644
index 0000000..ca3e343
--- /dev/null
+++ b/plugins/modules/vyos_reset.py
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_reset
+short_description: Execute reset commands on a VyOS device via REST API.
+description:
+ - Sends a C(reset) operational command to a VyOS device via the HTTPS REST
+ API (C(/reset) endpoint).
+ - Useful for resetting BGP sessions, VPN tunnels, ARP caches, etc.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ path:
+ description:
+ - Reset command path tokens.
+ - E.g. C(["ip", "bgp", "192.0.2.1"]) to reset a BGP peer.
+ type: list
+ elements: str
+ required: true
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+examples: |
+ - name: Reset a BGP peer
+ vyos.rest.vyos_reset:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["ip", "bgp", "192.0.2.11"]
+
+ - name: Reset an IPsec VPN peer
+ vyos.rest.vyos_reset:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["vpn", "ipsec-peer", "203.0.113.5"]
+
+ - name: Clear ARP cache
+ vyos.rest.vyos_reset:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["arp", "cache"]
+"""
+
+RETURN = r"""
+output:
+ description: Text output from the reset command (usually empty on success).
+ returned: success
+ type: str
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ path=dict(type="list", elements="str", required=True),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=False,
+ )
+
+ client = VyOSRestClient(module)
+ try:
+ result = client.reset(module.params["path"])
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=True, output=result.get("data", ""))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_route_maps.py b/plugins/modules/vyos_route_maps.py
new file mode 100644
index 0000000..d13a88d
--- /dev/null
+++ b/plugins/modules/vyos_route_maps.py
@@ -0,0 +1,376 @@
+#!/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_route_maps
+short_description: Route Map resource module via REST API.
+description:
+ - Manages route maps on VyOS via the HTTPS REST API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of route-map configurations.
+ type: list
+ elements: dict
+ suboptions:
+ route_map:
+ description: Route map name.
+ type: str
+ entries:
+ description: Route map rules.
+ type: list
+ elements: dict
+ suboptions:
+ sequence:
+ description: Rule number (1-65535).
+ type: int
+ action:
+ description: permit or deny.
+ type: str
+ choices: [permit, deny]
+ description:
+ type: str
+ call:
+ description: Call another route map.
+ type: str
+ continue_sequence:
+ description: Continue at a different sequence.
+ type: int
+ match:
+ description: Match conditions.
+ type: dict
+ suboptions:
+ prefix_list:
+ type: str
+ prefix_list6:
+ type: str
+ interface:
+ type: str
+ metric:
+ type: int
+ origin:
+ type: str
+ ip:
+ type: dict
+ suboptions:
+ nexthop_address:
+ type: str
+ nexthop_prefix_list:
+ type: str
+ ipv6:
+ type: dict
+ suboptions:
+ nexthop_address:
+ type: str
+ set:
+ description: Route parameters to set.
+ type: dict
+ suboptions:
+ aggregator:
+ type: dict
+ suboptions:
+ as:
+ type: str
+ ip:
+ type: str
+ as_path_prepend:
+ type: str
+ atomic_aggregate:
+ type: bool
+ bgp_extcommunity_rt:
+ type: str
+ community:
+ type: dict
+ suboptions:
+ value:
+ type: str
+ ip_next_hop:
+ type: str
+ ipv6_next_hop:
+ type: dict
+ suboptions:
+ ip_type:
+ type: str
+ choices: [global, local]
+ value:
+ type: str
+ large_community:
+ type: str
+ local_preference:
+ type: int
+ metric:
+ type: str
+ metric_type:
+ type: str
+ origin:
+ type: str
+ originator_id:
+ type: str
+ src:
+ type: str
+ tag:
+ type: str
+ weight:
+ type: int
+ state:
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+before:
+ returned: always
+ type: list
+after:
+ returned: when changed
+ type: list
+commands:
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def _get(client):
+ try:
+ r = client.retrieve_show_config(["policy", "route-map"])
+ data = r.get("data") or {}
+ return [{"route_map": name, "raw": rdata} for name, rdata in data.items()]
+ except VyOSRestError:
+ return []
+
+
+def _apply(client, rm_cfg, commands):
+ rm_name = rm_cfg["route_map"]
+ for rule in rm_cfg.get("entries") or []:
+ seq = str(rule["sequence"])
+ base = ["policy", "route-map", rm_name, "rule", seq]
+ client.configure_set(base)
+ commands.append("set policy route-map {n} rule {s}".format(n=rm_name, s=seq))
+
+ if rule.get("action"):
+ client.configure_set(base + ["action"], rule["action"])
+ commands.append(
+ "set policy route-map {n} rule {s} action {a}".format(
+ n=rm_name,
+ s=seq,
+ a=rule["action"],
+ ),
+ )
+ if rule.get("description"):
+ client.configure_set(base + ["description"], rule["description"])
+ if rule.get("call"):
+ client.configure_set(base + ["call"], rule["call"])
+ if rule.get("continue_sequence"):
+ client.configure_set(base + ["continue"], str(rule["continue_sequence"]))
+
+ # Match conditions
+ match = rule.get("match") or {}
+ if match.get("prefix_list"):
+ client.configure_set(
+ base + ["match", "ip", "address", "prefix-list"],
+ match["prefix_list"],
+ )
+ if match.get("interface"):
+ client.configure_set(base + ["match", "interface"], match["interface"])
+ if match.get("metric") is not None:
+ client.configure_set(base + ["match", "metric"], str(match["metric"]))
+ ip_match = match.get("ip") or {}
+ if ip_match.get("nexthop_address"):
+ client.configure_set(
+ base + ["match", "ip", "nexthop", "address"],
+ ip_match["nexthop_address"],
+ )
+
+ # Set actions
+ setv = rule.get("set") or {}
+ if setv.get("ip_next_hop"):
+ client.configure_set(base + ["set", "ip-next-hop"], setv["ip_next_hop"])
+ commands.append(
+ "set policy route-map {n} rule {s} set ip-next-hop {nh}".format(
+ n=rm_name,
+ s=seq,
+ nh=setv["ip_next_hop"],
+ ),
+ )
+ if setv.get("local_preference") is not None:
+ client.configure_set(
+ base + ["set", "local-preference"],
+ str(setv["local_preference"]),
+ )
+ if setv.get("metric"):
+ client.configure_set(base + ["set", "metric"], str(setv["metric"]))
+ if setv.get("origin"):
+ client.configure_set(base + ["set", "origin"], setv["origin"])
+ if setv.get("weight") is not None:
+ client.configure_set(base + ["set", "weight"], str(setv["weight"]))
+ if setv.get("as_path_prepend"):
+ client.configure_set(base + ["set", "as-path-prepend"], setv["as_path_prepend"])
+ comm = setv.get("community") or {}
+ if comm.get("value"):
+ client.configure_set(base + ["set", "community", comm["value"]])
+ if setv.get("tag"):
+ client.configure_set(base + ["set", "tag"], setv["tag"])
+ if setv.get("src"):
+ client.configure_set(base + ["set", "src"], setv["src"])
+ nh6 = setv.get("ipv6_next_hop") or {}
+ if nh6.get("value"):
+ ip_type = nh6.get("ip_type", "global")
+ client.configure_set(base + ["set", "ipv6-next-hop", ip_type], nh6["value"])
+
+
+def main():
+ set_spec = dict(
+ type="dict",
+ options=dict(
+ aggregator=dict(type="dict", options=dict(as_=dict(type="str"), ip=dict(type="str"))),
+ as_path_prepend=dict(type="str"),
+ atomic_aggregate=dict(type="bool"),
+ bgp_extcommunity_rt=dict(type="str"),
+ community=dict(type="dict", options=dict(value=dict(type="str"))),
+ ip_next_hop=dict(type="str"),
+ ipv6_next_hop=dict(
+ type="dict",
+ options=dict(
+ ip_type=dict(type="str", choices=["global", "local"]),
+ value=dict(type="str"),
+ ),
+ ),
+ large_community=dict(type="str"),
+ local_preference=dict(type="int"),
+ metric=dict(type="str"),
+ metric_type=dict(type="str"),
+ origin=dict(type="str"),
+ originator_id=dict(type="str"),
+ src=dict(type="str"),
+ tag=dict(type="str"),
+ weight=dict(type="int"),
+ ),
+ )
+ match_spec = dict(
+ type="dict",
+ options=dict(
+ prefix_list=dict(type="str"),
+ prefix_list6=dict(type="str"),
+ interface=dict(type="str"),
+ metric=dict(type="int"),
+ origin=dict(type="str"),
+ ip=dict(
+ type="dict",
+ options=dict(
+ nexthop_address=dict(type="str"),
+ nexthop_prefix_list=dict(type="str"),
+ ),
+ ),
+ ipv6=dict(type="dict", options=dict(nexthop_address=dict(type="str"))),
+ ),
+ )
+
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ route_map=dict(type="str"),
+ entries=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ sequence=dict(type="int"),
+ action=dict(type="str", choices=["permit", "deny"]),
+ description=dict(type="str"),
+ call=dict(type="str"),
+ continue_sequence=dict(type="int"),
+ match=match_spec,
+ set=set_spec,
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+ before = _get(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state == "deleted" and not config:
+ try:
+ client.configure_delete(["policy", "route-map"])
+ commands.append("delete policy route-map")
+ changed = True
+ except VyOSRestError:
+ pass
+ elif state == "deleted" and config:
+ for rm in config:
+ try:
+ client.configure_delete(["policy", "route-map", rm["route_map"]])
+ commands.append("delete policy route-map {n}".format(n=rm["route_map"]))
+ changed = True
+ except VyOSRestError:
+ pass
+ elif state in ("merged", "replaced", "overridden"):
+ if state in ("replaced", "overridden"):
+ for rm in config:
+ try:
+ client.configure_delete(["policy", "route-map", rm["route_map"]])
+ except VyOSRestError:
+ pass
+ for rm in config:
+ _apply(client, rm, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_show.py b/plugins/modules/vyos_show.py
new file mode 100644
index 0000000..fbe6c1b
--- /dev/null
+++ b/plugins/modules/vyos_show.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_show
+short_description: Execute op-mode show commands on a VyOS device via REST API.
+description:
+ - Sends a C(show) operational-mode command to a VyOS device via the
+ HTTPS REST API (C(/show) endpoint) and returns the text output.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ path:
+ description:
+ - Operational-mode command tokens following C(show).
+ - E.g. C(["interfaces"]) maps to C(show interfaces).
+ type: list
+ elements: str
+ required: true
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+notes:
+ - This module never modifies device state.
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.rest.vyos_configure
+ - module: vyos.rest.vyos_retrieve
+examples: |
+ - name: Show interfaces
+ vyos.rest.vyos_show:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["interfaces"]
+ register: iface_output
+
+ - name: Show version
+ vyos.rest.vyos_show:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["version"]
+ register: ver_output
+
+ - name: Show installed images
+ vyos.rest.vyos_show:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ path: ["system", "image"]
+ register: images
+"""
+
+RETURN = r"""
+output:
+ description: Raw text output from the show command.
+ returned: success
+ type: str
+ sample: |
+ Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
+ Interface IP Address S/L Description
+ eth0 10.0.0.1/24 u/u WAN
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ path=dict(type="list", elements="str", required=True),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ try:
+ result = client.show(module.params["path"])
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=False, output=result.get("data", ""))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_snmp_server.py b/plugins/modules/vyos_snmp_server.py
index a092432..9bdf451 100644
--- a/plugins/modules/vyos_snmp_server.py
+++ b/plugins/modules/vyos_snmp_server.py
@@ -1,8 +1,6 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-# Copyright 2024 Red Hat
# GNU General Public License v3.0+
-# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
@@ -15,12 +13,11 @@ module: vyos_snmp_server
short_description: Manage SNMP server configuration on VyOS devices using REST API
description:
- Manages SNMP server configuration on VyOS devices via the REST API.
- - Supports communities, listen addresses, contact/location/description scalar
- fields, trap target, and SNMPv3 (engine ID, groups, users, views).
+ - Supports communities, listen addresses, contact/location/description,
+ trap target, and SNMPv3 (engine ID, groups, users, views).
- Uses REST API (C(connection=httpapi)) instead of CLI.
version_added: "1.0.0"
author: your_name (@yourhandle)
-
options:
config:
description: SNMP server configuration.
@@ -79,13 +76,10 @@ options:
type: dict
suboptions:
address:
- description: IP address of trap target.
type: str
community:
- description: Community string for traps.
type: str
port:
- description: Destination port for trap notifications.
type: int
snmp_v3:
description: SNMPv3 configuration.
@@ -95,150 +89,108 @@ options:
description: EngineID as a hex string.
type: str
groups:
- description: SNMPv3 groups.
type: list
elements: dict
suboptions:
group:
- description: Group name.
type: str
required: true
mode:
- description: Access mode.
type: str
choices: ['ro', 'rw']
seclevel:
- description: Security level.
type: str
choices: ['auth', 'priv']
view:
- description: View name for this group.
type: str
users:
- description: SNMPv3 users.
type: list
elements: dict
suboptions:
user:
- description: Username.
type: str
required: true
authentication:
- description: Authentication configuration.
type: dict
suboptions:
type:
- description: Authentication protocol.
type: str
choices: ['md5', 'sha']
encrypted_key:
- description: Encrypted authentication password.
+ description: Encrypted key (stored as encrypted-password on device).
type: str
plaintext_key:
- description: Plaintext authentication password (will be encrypted on device).
+ description: Plaintext key (device encrypts it; recommended for 1.5+).
type: str
no_log: true
privacy:
- description: Privacy configuration.
type: dict
suboptions:
type:
- description: Privacy protocol.
type: str
choices: ['des', 'aes']
encrypted_key:
- description: Encrypted privacy password.
type: str
plaintext_key:
- description: Plaintext privacy password (will be encrypted on device).
type: str
no_log: true
group:
- description: Group membership for this user.
type: str
mode:
- description: Access mode.
type: str
choices: ['ro', 'rw']
tsm_key:
- description: TSM certificate fingerprint or filename.
type: str
trap_targets:
- description: SNMPv3 trap/inform targets.
type: list
elements: dict
suboptions:
address:
- description: IP/IPv6 address of trap target.
type: str
port:
- description: TCP/UDP port for traps/informs.
type: int
protocol:
- description: Notification protocol.
type: str
choices: ['tcp', 'udp']
type:
- description: Notification type.
type: str
choices: ['inform', 'trap']
authentication:
- description: Authentication for this trap target.
type: dict
suboptions:
type:
- description: Authentication protocol.
type: str
- choices: ['md5', 'sha']
encrypted_key:
- description: Encrypted authentication password.
type: str
plaintext_key:
- description: Plaintext authentication password (will be encrypted on device).
type: str
no_log: true
privacy:
- description: Privacy for this trap target.
type: dict
suboptions:
type:
- description: Privacy protocol.
type: str
- choices: ['des', 'aes']
encrypted_key:
- description: Encrypted privacy password.
type: str
plaintext_key:
- description: Plaintext privacy password (will be encrypted on device).
type: str
no_log: true
views:
- description: SNMPv3 views.
type: list
elements: dict
suboptions:
view:
- description: View name.
type: str
required: true
oid:
- description: OID for this view.
type: str
exclude:
- description: Excluded OID subtree.
type: str
mask:
- description: Bit-mask for OID subidentifiers.
type: str
-
state:
description:
- Desired state of SNMP configuration.
- - C(merged) adds or updates provided config without removing existing entries.
- - C(replaced) and C(overridden) perform a full replacement — entries not in
- the task config are removed.
- - C(deleted) removes all SNMP configuration with a single delete command.
- - C(gathered) returns current device config as structured data, no changes made.
type: str
default: merged
choices:
@@ -247,9 +199,6 @@ options:
- overridden
- deleted
- gathered
-
-notes:
- - Tested against VyOS 1.5q2.
"""
EXAMPLES = r"""
@@ -279,46 +228,10 @@ EXAMPLES = r"""
type: aes
state: merged
-# ------------------------------------------------------------------------
-
-- name: Replace SNMP configuration
- vyos.rest.vyos_snmp_server:
- config:
- communities:
- - name: bridges
- networks:
- - 1.1.1.0/24
- - 12.1.1.0/24
- location: "RDU, NC"
- listen_addresses:
- - address: 100.1.2.1
- port: 33
- snmp_v3:
- groups:
- - group: default
- view: default
- users:
- - user: admin_user
- authentication:
- encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633
- type: sha
- privacy:
- encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633
- type: aes
- group: default
- views:
- - view: default
- oid: "1"
- state: replaced
-
-# ------------------------------------------------------------------------
-
- name: Delete all SNMP configuration
vyos.rest.vyos_snmp_server:
state: deleted
-# ------------------------------------------------------------------------
-
- name: Gather current SNMP configuration
vyos.rest.vyos_snmp_server:
state: gathered
@@ -327,46 +240,32 @@ EXAMPLES = r"""
RETURN = r"""
before:
description: SNMP configuration before this module ran.
- returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted)
+ returned: when state is merged, replaced, overridden or deleted
type: dict
-
after:
description: SNMP configuration after this module ran.
returned: when changed
type: dict
-
commands:
description: List of API command dicts sent to the device.
returned: always
type: list
-
gathered:
description: Current SNMP configuration as structured data.
- returned: when I(state) is C(gathered)
+ returned: when state is gathered
type: dict
-
-response:
- description: Raw API response.
- returned: when changes are applied
- type: dict
-
saved:
- description: Result of save_config after applying changes.
+ description: Whether the config was saved after changes.
returned: when changes are applied
- type: dict
+ type: bool
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
-# ------------------------------------------------------------
-# Constants
-# ------------------------------------------------------------
-
SNMP_BASE = ["service", "snmp"]
-# Scalar fields that map directly: argspec_key → api_key
SCALAR_FIELDS = {
"contact": "contact",
"description": "description",
@@ -376,13 +275,7 @@ SCALAR_FIELDS = {
}
-# ------------------------------------------------------------
-# Generic helpers
-# ------------------------------------------------------------
-
-
def to_list(value):
- """Coerce None / str / list → list."""
if value is None:
return []
if isinstance(value, list):
@@ -394,16 +287,19 @@ def to_list(value):
return [str(value)]
-# ------------------------------------------------------------
-# Parsing: API response → Ansible argspec dict
-# ------------------------------------------------------------
+def _cmd(op, path):
+ return {"op": op, "path": path}
+
+
+def _set(path):
+ return _cmd("set", path)
+
+
+def _delete(path):
+ return _cmd("delete", path)
def _parse_communities(raw):
- """
- API: {"bridges": {"client": [...], "network": [...]}, "switches": {"authorization": "rw"}}
- → [{"name": "bridges", "clients": [...], ...}, ...]
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -423,10 +319,6 @@ def _parse_communities(raw):
def _parse_listen_addresses(raw):
- """
- API: {"198.51.100.10": {"port": "33"}, "203.0.113.65": {}}
- → [{"address": "198.51.100.10", "port": 33}, {"address": "203.0.113.65"}]
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -439,9 +331,6 @@ def _parse_listen_addresses(raw):
def _parse_trap_target(raw):
- """
- API: {"address": "x.x.x.x"} or plain string address, optional port/community.
- """
if not raw:
return None
if isinstance(raw, str):
@@ -457,19 +346,12 @@ def _parse_trap_target(raw):
def _parse_v3_auth_privacy(raw, key):
- """
- Parse auth or privacy block.
- API key is 'auth', argspec key is 'authentication'.
- API key is 'privacy', argspec key is 'privacy'.
- Returns dict with type and encrypted_key (never plaintext — device never returns it).
- """
block = raw.get(key) if isinstance(raw, dict) else None
if not block:
return None
result = {}
if "type" in block:
result["type"] = block["type"]
- # API uses 'encrypted-password'; map to argspec 'encrypted_key'
if "encrypted-password" in block:
result["encrypted_key"] = block["encrypted-password"]
if "plaintext-key" in block:
@@ -478,10 +360,6 @@ def _parse_v3_auth_privacy(raw, key):
def _parse_v3_users(raw):
- """
- API: {"adminuser": {"auth": {...}, "group": "testgroup", "privacy": {...}}}
- → [{"user": "adminuser", "authentication": {...}, "group": "testgroup", "privacy": {...}}]
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -505,10 +383,6 @@ def _parse_v3_users(raw):
def _parse_v3_groups(raw):
- """
- API: {"testgroup": {"mode": "ro", "view": "default"}}
- → [{"group": "testgroup", "mode": "ro", "view": "default"}]
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -523,12 +397,6 @@ def _parse_v3_groups(raw):
def _parse_v3_views(raw):
- """
- API: {"default": {"oid": {"1": {}}}}
- → [{"view": "default", "oid": "1"}]
-
- OID is stored as a dict key with an empty dict value.
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -537,7 +405,6 @@ def _parse_v3_views(raw):
if isinstance(data, dict) and "oid" in data:
oid_data = data["oid"]
if isinstance(oid_data, dict) and oid_data:
- # Extract the first (and usually only) OID key
entry["oid"] = str(list(oid_data.keys())[0])
elif isinstance(oid_data, str):
entry["oid"] = oid_data
@@ -551,10 +418,6 @@ def _parse_v3_views(raw):
def _parse_v3_trap_targets(raw):
- """
- API: {"x.x.x.x": {"auth": {...}, "privacy": {...}, "port": "162", ...}}
- → [{"address": "x.x.x.x", ...}]
- """
if not raw or not isinstance(raw, dict):
return []
result = []
@@ -578,35 +441,21 @@ def _parse_v3_trap_targets(raw):
def parse_snmp_config(raw):
- """
- Convert full API SNMP response dict → Ansible argspec dict.
- """
if not raw or not isinstance(raw, dict):
return {}
-
result = {}
-
- # Scalar fields
for argspec_key, api_key in SCALAR_FIELDS.items():
if api_key in raw:
result[argspec_key] = raw[api_key]
-
- # Communities
communities = _parse_communities(raw.get("community"))
if communities:
result["communities"] = communities
-
- # Listen addresses
listen = _parse_listen_addresses(raw.get("listen-address"))
if listen:
result["listen_addresses"] = listen
-
- # Trap target
trap = _parse_trap_target(raw.get("trap-target"))
if trap:
result["trap_target"] = trap
-
- # SNMPv3
v3_raw = raw.get("v3")
if v3_raw and isinstance(v3_raw, dict):
v3 = {}
@@ -626,12 +475,10 @@ def parse_snmp_config(raw):
v3["trap_targets"] = trap_targets
if v3:
result["snmp_v3"] = v3
-
return result
def get_running_config(vyos):
- """Fetch and parse current SNMP config from device."""
try:
raw = vyos.get_config(SNMP_BASE)
except Exception as e:
@@ -641,25 +488,7 @@ def get_running_config(vyos):
return parse_snmp_config(raw)
-# ------------------------------------------------------------
-# Command builders: Ansible argspec dict → API command list
-# ------------------------------------------------------------
-
-
-def _cmd(op, path):
- return {"op": op, "path": path}
-
-
-def _set(path):
- return _cmd("set", path)
-
-
-def _delete(path):
- return _cmd("delete", path)
-
-
def _build_scalar_commands(want, have, state):
- """Build commands for simple string scalar fields."""
cmds = []
for argspec_key, api_key in SCALAR_FIELDS.items():
want_val = want.get(argspec_key)
@@ -675,90 +504,69 @@ def _build_scalar_commands(want, have, state):
def _build_community_commands(want_list, have_list, state):
- """Build commands for SNMP communities."""
cmds = []
want_map = {c["name"]: c for c in (want_list or [])}
have_map = {c["name"]: c for c in (have_list or [])}
-
if state in ("replaced", "overridden"):
- # Delete communities not in want
for name in have_map:
if name not in want_map:
cmds.append(_delete(SNMP_BASE + ["community", name]))
-
for name, want_comm in want_map.items():
have_comm = have_map.get(name, {})
base = SNMP_BASE + ["community", name]
-
- if state in ("merged", "replaced", "overridden"):
- # authorization_type
- want_auth = want_comm.get("authorization_type")
- have_auth = have_comm.get("authorization_type")
- if want_auth and want_auth != have_auth:
- cmds.append(_set(base + ["authorization", want_auth]))
- if state in ("replaced", "overridden") and have_auth and want_auth != have_auth:
- cmds.append(_delete(base + ["authorization"]))
-
- # clients
- want_clients = set(want_comm.get("clients") or [])
- have_clients = set(have_comm.get("clients") or [])
- for c in want_clients - have_clients:
- cmds.append(_set(base + ["client", c]))
- if state in ("replaced", "overridden"):
- for c in have_clients - want_clients:
- cmds.append(_delete(base + ["client", c]))
-
- # networks
- want_nets = set(want_comm.get("networks") or [])
- have_nets = set(have_comm.get("networks") or [])
- for n in want_nets - have_nets:
- cmds.append(_set(base + ["network", n]))
- if state in ("replaced", "overridden"):
- for n in have_nets - want_nets:
- cmds.append(_delete(base + ["network", n]))
-
+ want_auth = want_comm.get("authorization_type")
+ have_auth = have_comm.get("authorization_type")
+ if want_auth and want_auth != have_auth:
+ cmds.append(_set(base + ["authorization", want_auth]))
+ if state in ("replaced", "overridden") and have_auth and want_auth != have_auth:
+ cmds.append(_delete(base + ["authorization"]))
+ want_clients = set(want_comm.get("clients") or [])
+ have_clients = set(have_comm.get("clients") or [])
+ for c in want_clients - have_clients:
+ cmds.append(_set(base + ["client", c]))
+ if state in ("replaced", "overridden"):
+ for c in have_clients - want_clients:
+ cmds.append(_delete(base + ["client", c]))
+ want_nets = set(want_comm.get("networks") or [])
+ have_nets = set(have_comm.get("networks") or [])
+ for n in want_nets - have_nets:
+ cmds.append(_set(base + ["network", n]))
+ if state in ("replaced", "overridden"):
+ for n in have_nets - want_nets:
+ cmds.append(_delete(base + ["network", n]))
return cmds
def _build_listen_address_commands(want_list, have_list, state):
- """Build commands for listen-address entries."""
cmds = []
want_map = {e["address"]: e for e in (want_list or [])}
have_map = {e["address"]: e for e in (have_list or [])}
base = SNMP_BASE + ["listen-address"]
-
if state in ("replaced", "overridden"):
for addr in have_map:
if addr not in want_map:
cmds.append(_delete(base + [addr]))
-
for addr, want_entry in want_map.items():
have_entry = have_map.get(addr, {})
want_port = want_entry.get("port")
have_port = have_entry.get("port")
-
if addr not in have_map:
- # New address
if want_port:
cmds.append(_set(base + [addr, "port", str(want_port)]))
else:
cmds.append(_set(base + [addr]))
elif want_port != have_port:
- # Address exists but port changed — delete and re-set
cmds.append(_delete(base + [addr]))
if want_port:
cmds.append(_set(base + [addr, "port", str(want_port)]))
else:
cmds.append(_set(base + [addr]))
-
return cmds
def _build_trap_target_commands(want, have, state):
- """Build commands for the v2 trap-target scalar dict."""
cmds = []
base = SNMP_BASE + ["trap-target"]
-
if state in ("merged", "replaced", "overridden"):
if want:
want_addr = want.get("address")
@@ -769,25 +577,18 @@ def _build_trap_target_commands(want, have, state):
cmds.append(_set(base + [want_addr, "community", want["community"]]))
if want.get("port"):
cmds.append(_set(base + [want_addr, "port", str(want["port"])]))
-
if state in ("replaced", "overridden"):
if have and (not want or have.get("address") != (want or {}).get("address")):
cmds.append(_delete(base))
-
return cmds
def _build_v3_auth_privacy_commands(base, want_block, have_block, api_key):
- """
- Build set commands for a v3 auth or privacy block.
- api_key: 'auth' or 'privacy'
- """
cmds = []
if not want_block:
return cmds
block_base = base + [api_key]
have_block = have_block or {}
-
if want_block.get("type") and want_block["type"] != have_block.get("type"):
cmds.append(_set(block_base + ["type", want_block["type"]]))
if want_block.get("encrypted_key") and want_block["encrypted_key"] != have_block.get(
@@ -795,28 +596,22 @@ def _build_v3_auth_privacy_commands(base, want_block, have_block, api_key):
):
cmds.append(_set(block_base + ["encrypted-password", want_block["encrypted_key"]]))
if want_block.get("plaintext_key"):
- # Always set plaintext — we cannot compare it to the stored encrypted value
cmds.append(_set(block_base + ["plaintext-key", want_block["plaintext_key"]]))
-
return cmds
def _build_v3_user_commands(want_list, have_list, state):
- """Build commands for SNMPv3 users."""
cmds = []
want_map = {u["user"]: u for u in (want_list or [])}
have_map = {u["user"]: u for u in (have_list or [])}
base = SNMP_BASE + ["v3", "user"]
-
if state in ("replaced", "overridden"):
for username in have_map:
if username not in want_map:
cmds.append(_delete(base + [username]))
-
for username, want_user in want_map.items():
have_user = have_map.get(username, {})
user_base = base + [username]
-
cmds += _build_v3_auth_privacy_commands(
user_base,
want_user.get("authentication"),
@@ -829,33 +624,27 @@ def _build_v3_user_commands(want_list, have_list, state):
have_user.get("privacy"),
"privacy",
)
-
if want_user.get("group") and want_user["group"] != have_user.get("group"):
cmds.append(_set(user_base + ["group", want_user["group"]]))
if want_user.get("mode") and want_user["mode"] != have_user.get("mode"):
cmds.append(_set(user_base + ["mode", want_user["mode"]]))
if want_user.get("tsm_key") and want_user["tsm_key"] != have_user.get("tsm_key"):
cmds.append(_set(user_base + ["tsm-key", want_user["tsm_key"]]))
-
return cmds
def _build_v3_group_commands(want_list, have_list, state):
- """Build commands for SNMPv3 groups."""
cmds = []
want_map = {g["group"]: g for g in (want_list or [])}
have_map = {g["group"]: g for g in (have_list or [])}
base = SNMP_BASE + ["v3", "group"]
-
if state in ("replaced", "overridden"):
for name in have_map:
if name not in want_map:
cmds.append(_delete(base + [name]))
-
for name, want_group in want_map.items():
have_group = have_map.get(name, {})
group_base = base + [name]
-
for key, api_key in [("mode", "mode"), ("seclevel", "seclevel"), ("view", "view")]:
want_val = want_group.get(key)
have_val = have_group.get(key)
@@ -863,74 +652,56 @@ def _build_v3_group_commands(want_list, have_list, state):
cmds.append(_set(group_base + [api_key, want_val]))
if state in ("replaced", "overridden") and have_val and want_val != have_val:
cmds.append(_delete(group_base + [api_key]))
-
return cmds
def _build_v3_view_commands(want_list, have_list, state):
- """Build commands for SNMPv3 views."""
cmds = []
want_map = {v["view"]: v for v in (want_list or [])}
have_map = {v["view"]: v for v in (have_list or [])}
base = SNMP_BASE + ["v3", "view"]
-
if state in ("replaced", "overridden"):
for name in have_map:
if name not in want_map:
cmds.append(_delete(base + [name]))
-
for name, want_view in want_map.items():
have_view = have_map.get(name, {})
view_base = base + [name]
-
want_oid = str(want_view["oid"]) if want_view.get("oid") else None
have_oid = str(have_view.get("oid")) if have_view.get("oid") else None
-
if want_oid and want_oid != have_oid:
cmds.append(_set(view_base + ["oid", want_oid]))
if state in ("replaced", "overridden") and have_oid and want_oid != have_oid:
cmds.append(_delete(view_base + ["oid", have_oid]))
-
for key in ("exclude", "mask"):
want_val = want_view.get(key)
have_val = have_view.get(key)
if want_val and want_val != have_val:
cmds.append(_set(view_base + [key, want_val]))
-
return cmds
def _build_v3_commands(want_v3, have_v3, state):
- """Build all SNMPv3 commands."""
cmds = []
want_v3 = want_v3 or {}
have_v3 = have_v3 or {}
-
- # engine_id (maps to API key 'engineid')
want_eid = want_v3.get("engine_id")
have_eid = have_v3.get("engine_id")
if want_eid and want_eid != have_eid:
cmds.append(_set(SNMP_BASE + ["v3", "engineid", want_eid]))
if state in ("replaced", "overridden") and have_eid and want_eid != have_eid:
cmds.append(_delete(SNMP_BASE + ["v3", "engineid"]))
-
cmds += _build_v3_group_commands(want_v3.get("groups"), have_v3.get("groups"), state)
cmds += _build_v3_user_commands(want_v3.get("users"), have_v3.get("users"), state)
cmds += _build_v3_view_commands(want_v3.get("views"), have_v3.get("views"), state)
-
return cmds
def build_commands(want, have, state):
- """
- Build the full list of API command dicts to move from have → want.
- For deleted state, a single delete of the entire SNMP subtree is issued.
- """
if state == "deleted":
if have:
return [_delete(SNMP_BASE)]
return []
-
cmds = []
cmds += _build_scalar_commands(want, have, state)
cmds += _build_community_commands(want.get("communities"), have.get("communities"), state)
@@ -944,11 +715,6 @@ def build_commands(want, have, state):
return cmds
-# ------------------------------------------------------------
-# Argument spec
-# ------------------------------------------------------------
-
-
def _auth_privacy_spec():
return dict(
type=dict(type="str"),
@@ -1052,17 +818,8 @@ ARGUMENT_SPEC = dict(
)
-# ------------------------------------------------------------
-# Main
-# ------------------------------------------------------------
-
-
def main():
- module = AnsibleModule(
- argument_spec=ARGUMENT_SPEC,
- supports_check_mode=True,
- )
-
+ module = AnsibleModule(argument_spec=ARGUMENT_SPEC, supports_check_mode=True)
vyos = VyOSModule(module)
state = module.params["state"]
config = module.params.get("config") or {}
@@ -1072,8 +829,7 @@ def main():
if state == "gathered":
module.exit_json(changed=False, gathered=have)
- want = config # already in argspec shape from AnsibleModule
-
+ want = config
commands = build_commands(want, have, state)
if module.check_mode:
diff --git a/plugins/modules/vyos_static_routes.py b/plugins/modules/vyos_static_routes.py
new file mode 100644
index 0000000..f203603
--- /dev/null
+++ b/plugins/modules/vyos_static_routes.py
@@ -0,0 +1,397 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_static_routes
+short_description: Manage static routes on VyOS via the REST API.
+description:
+ - Manages IPv4 and IPv6 static routes on VyOS devices using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_static_routes) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of static route address-family configurations.
+ type: list
+ elements: dict
+ suboptions:
+ address_families:
+ description: List of address family route groups.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family indicator.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ routes:
+ description: List of static route entries.
+ type: list
+ elements: dict
+ suboptions:
+ dest:
+ description: Destination prefix in CIDR notation.
+ type: str
+ required: true
+ blackhole_config:
+ description: Blackhole route configuration.
+ type: dict
+ suboptions:
+ distance:
+ description: Administrative distance (1-255).
+ type: int
+ type:
+ description: Blackhole type.
+ type: str
+ next_hops:
+ description: List of next-hop addresses.
+ type: list
+ elements: dict
+ suboptions:
+ forward_router_address:
+ description: Next-hop IP address.
+ type: str
+ required: true
+ admin_distance:
+ description: Administrative distance for this next-hop.
+ type: int
+ enabled:
+ description: Whether this next-hop is enabled.
+ type: bool
+ default: true
+ interface:
+ description: Outgoing interface name.
+ type: str
+ state:
+ description:
+ - C(merged): Add routes (preserve existing).
+ - C(replaced): Replace routes for listed destinations.
+ - C(overridden): Replace the entire static route table.
+ - C(deleted): Remove listed (or all) static routes.
+ - C(gathered): Read static routes from device.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+ hostname:
+ description: IP address or FQDN of the VyOS device.
+ type: str
+ required: true
+ port:
+ description: HTTPS port for the REST API.
+ type: int
+ default: 443
+ api_key:
+ description: API key configured on the device.
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ description: Request timeout in seconds.
+ type: int
+ default: 30
+ verify_ssl:
+ description: Validate the device's TLS certificate.
+ type: bool
+ default: false
+requirements:
+ - VyOS 1.3+
+seealso:
+ - module: vyos.vyos.vyos_static_routes
+examples: |
+ - name: Add IPv4 static routes
+ vyos.rest.vyos_static_routes:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ config:
+ - address_families:
+ - afi: ipv4
+ routes:
+ - dest: 192.0.2.0/24
+ next_hops:
+ - forward_router_address: 10.0.0.1
+ - dest: 203.0.113.0/24
+ blackhole_config:
+ distance: 200
+ state: merged
+
+ - name: Delete all static routes
+ vyos.rest.vyos_static_routes:
+ hostname: 192.168.1.1
+ api_key: MY-KEY
+ state: deleted
+"""
+
+RETURN = r"""
+before:
+ description: Static route config before the module ran.
+ returned: always
+ type: list
+after:
+ description: Static route config after the module ran.
+ returned: when changed
+ type: list
+gathered:
+ description: Static routes read from device (state=gathered).
+ returned: when state is gathered
+ type: list
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_ROUTE_BASE = {
+ "ipv4": ["protocols", "static", "route"],
+ "ipv6": ["protocols", "static", "route6"],
+}
+
+
+def _get_static_routes(client):
+ try:
+ result = client.retrieve_show_config(["protocols", "static"])
+ data = result.get("data") or {}
+ out = []
+ for afi, route_key in [("ipv4", "route"), ("ipv6", "route6")]:
+ routes_data = data.get(route_key, {})
+ if not isinstance(routes_data, dict):
+ continue
+ routes = []
+ for dest, rdata in routes_data.items():
+ route_entry = {"dest": dest}
+ if isinstance(rdata, dict):
+ if "blackhole" in rdata:
+ bh = rdata["blackhole"]
+ bc = {}
+ if isinstance(bh, dict) and "distance" in bh:
+ bc["distance"] = int(bh["distance"])
+ route_entry["blackhole_config"] = bc
+ next_hops = []
+ for nh_key in ("next-hop", "next_hop"):
+ nh_data = rdata.get(nh_key, {})
+ if isinstance(nh_data, dict):
+ for nh_addr, nh_opts in nh_data.items():
+ nh = {"forward_router_address": nh_addr}
+ if isinstance(nh_opts, dict):
+ if "distance" in nh_opts:
+ nh["admin_distance"] = int(nh_opts["distance"])
+ nh["enabled"] = "disable" not in nh_opts
+ next_hops.append(nh)
+ if next_hops:
+ route_entry["next_hops"] = next_hops
+ routes.append(route_entry)
+ if routes:
+ out.append({"address_families": [{"afi": afi, "routes": routes}]})
+ return out
+ except VyOSRestError:
+ return []
+
+
+def _apply_route(client, afi, route, commands):
+ base = _ROUTE_BASE[afi]
+ dest = route["dest"]
+
+ if route.get("blackhole_config") is not None:
+ bh_path = base + [dest, "blackhole"]
+ client.configure_set(bh_path)
+ commands.append("set {p}".format(p=" ".join(bh_path)))
+ dist = route["blackhole_config"].get("distance")
+ if dist:
+ client.configure_set(bh_path + ["distance"], str(dist))
+ commands.append(
+ "set {p} distance {d}".format(p=" ".join(bh_path), d=dist),
+ )
+
+ for nh in route.get("next_hops") or []:
+ nh_addr = nh["forward_router_address"]
+ nh_path = base + [dest, "next-hop", nh_addr]
+ client.configure_set(nh_path)
+ commands.append("set {p}".format(p=" ".join(nh_path)))
+ if nh.get("admin_distance"):
+ client.configure_set(nh_path + ["distance"], str(nh["admin_distance"]))
+ commands.append(
+ "set {p} distance {d}".format(
+ p=" ".join(nh_path),
+ d=nh["admin_distance"],
+ ),
+ )
+ if "enabled" in nh and not nh["enabled"]:
+ client.configure_set(nh_path + ["disable"])
+ commands.append("set {p} disable".format(p=" ".join(nh_path)))
+ if nh.get("interface"):
+ client.configure_set(nh_path + ["interface"], nh["interface"])
+ commands.append(
+ "set {p} interface {i}".format(
+ p=" ".join(nh_path),
+ i=nh["interface"],
+ ),
+ )
+
+
+def main():
+ argument_spec = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address_families=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]),
+ routes=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ dest=dict(type="str", required=True),
+ blackhole_config=dict(
+ type="dict",
+ options=dict(
+ distance=dict(type="int"),
+ type=dict(type="str"),
+ ),
+ ),
+ next_hops=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ forward_router_address=dict(
+ type="str",
+ required=True,
+ ),
+ admin_distance=dict(type="int"),
+ enabled=dict(type="bool", default=True),
+ interface=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ config = module.params.get("config") or []
+ commands = []
+ changed = False
+
+ before = _get_static_routes(client)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=before, before=before, commands=[])
+
+ if module.check_mode:
+ module.exit_json(changed=True, before=before, commands=["(check mode)"])
+
+ try:
+ if state in ("overridden", "deleted") and not config:
+ # Delete all static routes
+ for afi_key in ("route", "route6"):
+ try:
+ client.configure_delete(["protocols", "static", afi_key])
+ commands.append(
+ "delete protocols static {k}".format(k=afi_key),
+ )
+ except VyOSRestError:
+ pass
+ changed = True
+
+ elif state == "deleted" and config:
+ for entry in config:
+ for af in entry.get("address_families") or []:
+ afi = af["afi"]
+ base = _ROUTE_BASE[afi]
+ for route in af.get("routes") or []:
+ try:
+ client.configure_delete(base + [route["dest"]])
+ commands.append(
+ "delete {p} {d}".format(
+ p=" ".join(base),
+ d=route["dest"],
+ ),
+ )
+ except VyOSRestError:
+ pass
+ changed = True
+
+ elif state in ("merged", "replaced", "overridden"):
+ if state in ("replaced", "overridden"):
+ # Remove existing routes for affected destinations
+ dests_by_afi = {}
+ for entry in config:
+ for af in entry.get("address_families") or []:
+ afi = af["afi"]
+ dests_by_afi.setdefault(afi, set())
+ for route in af.get("routes") or []:
+ dests_by_afi[afi].add(route["dest"])
+ if state == "overridden":
+ for afi_key in ("route", "route6"):
+ try:
+ client.configure_delete(
+ ["protocols", "static", afi_key],
+ )
+ commands.append(
+ "delete protocols static {k}".format(k=afi_key),
+ )
+ except VyOSRestError:
+ pass
+ else:
+ for afi, dests in dests_by_afi.items():
+ base = _ROUTE_BASE[afi]
+ for dest in dests:
+ try:
+ client.configure_delete(base + [dest])
+ commands.append(
+ "delete {p} {d}".format(
+ p=" ".join(base),
+ d=dest,
+ ),
+ )
+ except VyOSRestError:
+ pass
+
+ for entry in config:
+ for af in entry.get("address_families") or []:
+ afi = af["afi"]
+ for route in af.get("routes") or []:
+ _apply_route(client, afi, route, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ after = _get_static_routes(client) if changed else before
+ module.exit_json(changed=changed, before=before, after=after, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py
new file mode 100644
index 0000000..d1d696e
--- /dev/null
+++ b/plugins/modules/vyos_system.py
@@ -0,0 +1,147 @@
+#!/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_system
+short_description: Manage system settings on VyOS via the REST API.
+description:
+ - Manages system-level settings — hostname, domain name, name servers,
+ domain search list — using the VyOS HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_system) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ host_name:
+ description: System hostname.
+ type: str
+ domain_name:
+ description: System domain name.
+ type: str
+ name_server:
+ description: List of DNS name servers. Mutually exclusive with domain_search.
+ type: list
+ elements: str
+ aliases: [name_servers]
+ domain_search:
+ description: List of domain search suffixes. Mutually exclusive with name_server.
+ type: list
+ elements: str
+ state:
+ description: C(present) to apply, C(absent) to remove.
+ type: str
+ choices: [present, absent]
+ default: present
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def main():
+ argument_spec = dict(
+ host_name=dict(type="str"),
+ domain_name=dict(type="str"),
+ name_server=dict(type="list", elements="str", aliases=["name_servers"]),
+ domain_search=dict(type="list", elements="str"),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=[["name_server", "domain_search"]],
+ supports_check_mode=True,
+ )
+
+ if module.check_mode:
+ module.exit_json(changed=True, commands=["(check mode)"])
+
+ client = VyOSRestClient(module)
+ state = module.params["state"]
+ commands = []
+ changed = False
+
+ try:
+ if state == "present":
+ if module.params.get("host_name"):
+ client.configure_set(["system", "host-name"], module.params["host_name"])
+ commands.append("set system host-name '{h}'".format(h=module.params["host_name"]))
+ changed = True
+ if module.params.get("domain_name"):
+ client.configure_set(["system", "domain-name"], module.params["domain_name"])
+ commands.append(
+ "set system domain-name '{d}'".format(d=module.params["domain_name"]),
+ )
+ changed = True
+ for ns in module.params.get("name_server") or []:
+ client.configure_set(["system", "name-server"], ns)
+ commands.append("set system name-server {ns}".format(ns=ns))
+ changed = True
+ for ds in module.params.get("domain_search") or []:
+ client.configure_set(["system", "domain-search", "domain"], ds)
+ commands.append("set system domain-search domain {ds}".format(ds=ds))
+ changed = True
+ else:
+ if module.params.get("host_name"):
+ try:
+ client.configure_delete(["system", "host-name"])
+ commands.append("delete system host-name")
+ changed = True
+ except VyOSRestError:
+ pass
+ if module.params.get("domain_name"):
+ try:
+ client.configure_delete(["system", "domain-name"])
+ commands.append("delete system domain-name")
+ changed = True
+ except VyOSRestError:
+ pass
+ if module.params.get("name_server"):
+ try:
+ client.configure_delete(["system", "name-server"])
+ commands.append("delete system name-server")
+ changed = True
+ except VyOSRestError:
+ pass
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=changed, commands=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..41e014d
--- /dev/null
+++ b/plugins/modules/vyos_user.py
@@ -0,0 +1,282 @@
+#!/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 local users on VyOS via the REST API.
+description:
+ - Manages local user accounts on VyOS devices using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_user) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ aggregate:
+ description: List of user definitions. Mutually exclusive with I(name).
+ type: list
+ elements: dict
+ aliases: [users, collection]
+ suboptions:
+ name:
+ description: Username.
+ type: str
+ required: true
+ full_name:
+ description: Full name.
+ type: str
+ configured_password:
+ description: Plaintext password (will be hashed on device).
+ type: str
+ no_log: true
+ encrypted_password:
+ description: Pre-hashed password string.
+ type: str
+ no_log: true
+ update_password:
+ description: When to update the password.
+ type: str
+ choices: [always, on_create]
+ level:
+ description: User privilege level.
+ type: str
+ choices: [admin, operator]
+ public_keys:
+ description: SSH public keys for authentication.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Key identifier (e.g. user@host).
+ type: str
+ required: true
+ key:
+ description: Base64-encoded public key.
+ type: str
+ required: true
+ type:
+ description: Key type.
+ type: str
+ required: true
+ choices: [ssh-dss, ssh-rsa, ecdsa-sha2-nistp256,
+ ecdsa-sha2-nistp384, ssh-ed25519, ecdsa-sha2-nistp521]
+ state:
+ description: Present or absent.
+ type: str
+ choices: [present, absent]
+ name:
+ description: Single username. Mutually exclusive with I(aggregate).
+ type: str
+ full_name:
+ description: Full name for the single user.
+ type: str
+ configured_password:
+ description: Plaintext password.
+ type: str
+ no_log: true
+ encrypted_password:
+ description: Pre-hashed password.
+ type: str
+ no_log: true
+ update_password:
+ type: str
+ choices: [always, on_create]
+ level:
+ type: str
+ choices: [admin, operator]
+ public_keys:
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ type: str
+ required: true
+ key:
+ type: str
+ required: true
+ type:
+ type: str
+ required: true
+ state:
+ description: Whether to create (present) or remove (absent) the user.
+ type: str
+ choices: [present, absent]
+ default: present
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+_KEY_TYPES = [
+ "ssh-dss",
+ "ssh-rsa",
+ "ecdsa-sha2-nistp256",
+ "ecdsa-sha2-nistp384",
+ "ssh-ed25519",
+ "ecdsa-sha2-nistp521",
+]
+
+
+def _apply_user(client, user_def, commands, update_password="always"):
+ name = user_def["name"]
+ base = ["system", "login", "user", name]
+ client.configure_set(base)
+ commands.append("set system login user {n}".format(n=name))
+
+ if user_def.get("full_name"):
+ client.configure_set(base + ["full-name"], user_def["full_name"])
+ if user_def.get("level"):
+ client.configure_set(base + ["level"], user_def["level"])
+ if user_def.get("configured_password") and update_password in ("always",):
+ client.configure_set(
+ base + ["authentication", "plaintext-password"],
+ user_def["configured_password"],
+ )
+ commands.append(
+ "set system login user {n} authentication plaintext-password".format(n=name),
+ )
+ if user_def.get("encrypted_password"):
+ client.configure_set(
+ base + ["authentication", "encrypted-password"],
+ user_def["encrypted_password"],
+ )
+ for pk in user_def.get("public_keys") or []:
+ kb = base + ["authentication", "public-keys", pk["name"]]
+ client.configure_set(kb + ["key"], pk["key"])
+ client.configure_set(kb + ["type"], pk["type"])
+ commands.append(
+ "set system login user {n} authentication public-keys {k}".format(
+ n=name,
+ k=pk["name"],
+ ),
+ )
+
+
+def _delete_user(client, name, commands):
+ try:
+ client.configure_delete(["system", "login", "user", name])
+ commands.append("delete system login user {n}".format(n=name))
+ except VyOSRestError:
+ pass
+
+
+def main():
+ pk_spec = dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ key=dict(type="str", required=True),
+ type=dict(type="str", required=True, choices=_KEY_TYPES),
+ ),
+ )
+ user_spec = dict(
+ name=dict(type="str", required=True),
+ full_name=dict(type="str"),
+ configured_password=dict(type="str", no_log=True),
+ encrypted_password=dict(type="str", no_log=True),
+ update_password=dict(type="str", choices=["always", "on_create"]),
+ level=dict(type="str", choices=["admin", "operator"]),
+ public_keys=pk_spec,
+ state=dict(type="str", choices=["present", "absent"]),
+ )
+ argument_spec = dict(
+ aggregate=dict(
+ type="list",
+ elements="dict",
+ aliases=["users", "collection"],
+ options=user_spec,
+ ),
+ name=dict(type="str"),
+ full_name=dict(type="str"),
+ configured_password=dict(type="str", no_log=True),
+ encrypted_password=dict(type="str", no_log=True),
+ update_password=dict(type="str", choices=["always", "on_create"]),
+ level=dict(type="str", choices=["admin", "operator"]),
+ public_keys=pk_spec,
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=[["aggregate", "name"]],
+ supports_check_mode=True,
+ )
+ if module.check_mode:
+ module.exit_json(changed=True, commands=["(check mode)"])
+
+ client = VyOSRestClient(module)
+ commands = []
+ changed = False
+
+ # Build unified user list
+ users = module.params.get("aggregate") or []
+ if module.params.get("name"):
+ users = [
+ {
+ "name": module.params["name"],
+ "full_name": module.params.get("full_name"),
+ "configured_password": module.params.get("configured_password"),
+ "encrypted_password": module.params.get("encrypted_password"),
+ "update_password": module.params.get("update_password", "always"),
+ "level": module.params.get("level"),
+ "public_keys": module.params.get("public_keys"),
+ "state": module.params.get("state", "present"),
+ },
+ ]
+
+ try:
+ for u in users:
+ u_state = u.get("state") or module.params.get("state", "present")
+ if u_state == "absent":
+ _delete_user(client, u["name"], commands)
+ else:
+ _apply_user(
+ client,
+ u,
+ commands,
+ update_password=u.get("update_password") or "always",
+ )
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=changed, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/plugins/modules/vyos_vlan.py b/plugins/modules/vyos_vlan.py
new file mode 100644
index 0000000..18eca92
--- /dev/null
+++ b/plugins/modules/vyos_vlan.py
@@ -0,0 +1,197 @@
+#!/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_vlan
+short_description: Manage VLANs on VyOS network devices via REST API.
+description:
+ - Manages 802.1Q VLAN sub-interfaces on VyOS Ethernet interfaces using
+ the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_vlan) but uses the HTTP API.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ vlan_id:
+ description: VLAN ID (0-4094).
+ type: int
+ name:
+ description: VLAN name (used as description on the VIF).
+ type: str
+ address:
+ description: IP address for the VLAN interface.
+ type: str
+ interfaces:
+ description: List of physical interfaces to configure the VLAN on.
+ type: list
+ elements: str
+ aggregate:
+ description: List of VLAN definitions.
+ type: list
+ elements: dict
+ suboptions:
+ vlan_id:
+ type: int
+ required: true
+ name:
+ type: str
+ address:
+ type: str
+ interfaces:
+ type: list
+ elements: str
+ required: true
+ state:
+ type: str
+ choices: [present, absent]
+ state:
+ description: C(present) to configure, C(absent) to remove.
+ type: str
+ choices: [present, absent]
+ default: present
+ purge:
+ description: Remove VLANs not defined in aggregate.
+ type: bool
+ default: false
+ hostname:
+ type: str
+ required: true
+ port:
+ type: int
+ default: 443
+ api_key:
+ type: str
+ required: true
+ no_log: true
+ timeout:
+ type: int
+ default: 30
+ verify_ssl:
+ type: bool
+ default: false
+"""
+
+RETURN = r"""
+commands:
+ description: set/delete commands issued.
+ returned: always
+ type: list
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VYOS_REST_CONNECTION_ARGSPEC,
+ VyOSRestClient,
+ VyOSRestError,
+)
+
+
+def _apply_vlan(client, vlan, commands):
+ vid = str(vlan["vlan_id"])
+ for iface in vlan.get("interfaces") or []:
+ # Detect interface type
+ if iface.startswith("bond"):
+ itype = "bonding"
+ else:
+ itype = "ethernet"
+ vif_base = ["interfaces", itype, iface, "vif", vid]
+ client.configure_set(vif_base)
+ commands.append("set interfaces {t} {i} vif {v}".format(t=itype, i=iface, v=vid))
+ if vlan.get("name"):
+ client.configure_set(vif_base + ["description"], vlan["name"])
+ commands.append(
+ "set interfaces {t} {i} vif {v} description '{n}'".format(
+ t=itype,
+ i=iface,
+ v=vid,
+ n=vlan["name"],
+ ),
+ )
+ if vlan.get("address"):
+ client.configure_set(vif_base + ["address"], vlan["address"])
+ commands.append(
+ "set interfaces {t} {i} vif {v} address {a}".format(
+ t=itype,
+ i=iface,
+ v=vid,
+ a=vlan["address"],
+ ),
+ )
+
+
+def _delete_vlan(client, vlan, commands):
+ vid = str(vlan["vlan_id"])
+ for iface in vlan.get("interfaces") or []:
+ itype = "bonding" if iface.startswith("bond") else "ethernet"
+ try:
+ client.configure_delete(["interfaces", itype, iface, "vif", vid])
+ commands.append("delete interfaces {t} {i} vif {v}".format(t=itype, i=iface, v=vid))
+ except VyOSRestError:
+ pass
+
+
+def main():
+ vlan_spec = dict(
+ vlan_id=dict(type="int", required=True),
+ name=dict(type="str"),
+ address=dict(type="str"),
+ interfaces=dict(type="list", elements="str"),
+ state=dict(type="str", choices=["present", "absent"]),
+ )
+ argument_spec = dict(
+ vlan_id=dict(type="int"),
+ name=dict(type="str"),
+ address=dict(type="str"),
+ interfaces=dict(type="list", elements="str"),
+ aggregate=dict(type="list", elements="dict", options=vlan_spec),
+ state=dict(type="str", default="present", choices=["present", "absent"]),
+ purge=dict(type="bool", default=False),
+ )
+ argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC)
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=[["aggregate", "vlan_id"]],
+ supports_check_mode=True,
+ )
+ if module.check_mode:
+ module.exit_json(changed=True, commands=["(check mode)"])
+
+ client = VyOSRestClient(module)
+ commands = []
+ changed = False
+
+ vlans = module.params.get("aggregate") or []
+ if module.params.get("vlan_id"):
+ vlans = [
+ {
+ "vlan_id": module.params["vlan_id"],
+ "name": module.params.get("name"),
+ "address": module.params.get("address"),
+ "interfaces": module.params.get("interfaces") or [],
+ "state": module.params.get("state", "present"),
+ },
+ ]
+
+ try:
+ for vlan in vlans:
+ v_state = vlan.get("state") or module.params.get("state", "present")
+ if v_state == "absent":
+ _delete_vlan(client, vlan, commands)
+ else:
+ _apply_vlan(client, vlan, commands)
+ changed = True
+ except VyOSRestError as exc:
+ module.fail_json(msg=str(exc))
+
+ module.exit_json(changed=changed, commands=commands)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/unit/test_vyos_rest_client.py b/tests/unit/test_vyos_rest_client.py
new file mode 100644
index 0000000..d476ea9
--- /dev/null
+++ b/tests/unit/test_vyos_rest_client.py
@@ -0,0 +1,150 @@
+"""Unit tests for vyos_rest module_utils."""
+
+import json
+
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+
+# Minimal AnsibleModule mock
+class FakeModule:
+ def __init__(self, params):
+ self.params = params
+
+ def fail_json(self, **kwargs):
+ raise AssertionError("fail_json called: {0}".format(kwargs))
+
+
+def _make_client(params=None):
+ from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VyOSRestClient,
+ )
+
+ p = {
+ "hostname": "192.0.2.1",
+ "port": 443,
+ "api_key": "test-key",
+ "timeout": 10,
+ "verify_ssl": False,
+ }
+ if params:
+ p.update(params)
+ return VyOSRestClient(FakeModule(p))
+
+
+def _ok_response(data):
+ raw = json.dumps({"success": True, "data": data, "error": None})
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = raw.encode()
+ return mock_resp
+
+
+class TestVyOSRestClientInit:
+ def test_base_url(self):
+ client = _make_client()
+ assert client.base_url == "https://192.0.2.1:443"
+
+ def test_custom_port(self):
+ client = _make_client({"port": 8443})
+ assert client.base_url == "https://192.0.2.1:8443"
+
+
+class TestConfigureSet:
+ def test_set_path_only(self):
+ client = _make_client()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(None),
+ ) as mock_open:
+ client.configure_set(["interfaces", "ethernet", "eth0"])
+ call_args = mock_open.call_args
+ data_field = call_args[1]["data"] if "data" in call_args[1] else call_args[0][1]
+ assert '"op": "set"' in data_field
+ assert '"interfaces"' in data_field
+
+ def test_set_path_with_value(self):
+ client = _make_client()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(None),
+ ) as mock_open:
+ client.configure_set(["system", "host-name"], "vyos")
+ call_args = mock_open.call_args
+ data_field = call_args[1]["data"] if "data" in call_args[1] else call_args[0][1]
+ assert '"value": "vyos"' in data_field
+
+
+class TestConfigureDelete:
+ def test_delete(self):
+ client = _make_client()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(None),
+ ) as mock_open:
+ client.configure_delete(["protocols", "static", "route"])
+ call_args = mock_open.call_args
+ data_field = call_args[1]["data"] if "data" in call_args[1] else call_args[0][1]
+ assert '"op": "delete"' in data_field
+
+
+class TestRetrieve:
+ def test_show_config(self):
+ client = _make_client()
+ expected = {"interfaces": {"ethernet": {"eth0": {"address": "dhcp"}}}}
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(expected),
+ ):
+ result = client.retrieve_show_config([])
+ assert result["data"] == expected
+
+ def test_exists_true(self):
+ client = _make_client()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(True),
+ ):
+ assert client.retrieve_exists(["service", "ntp"]) is True
+
+ def test_exists_false(self):
+ client = _make_client()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=_ok_response(False),
+ ):
+ assert client.retrieve_exists(["service", "nonexistent"]) is False
+
+
+class TestErrorHandling:
+ def test_api_failure_raises(self):
+ from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VyOSRestError,
+ )
+
+ client = _make_client()
+ err_response = MagicMock()
+ err_response.read.return_value = json.dumps(
+ {"success": False, "data": None, "error": "path not found"},
+ ).encode()
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=err_response,
+ ):
+ with pytest.raises(VyOSRestError, match="path not found"):
+ client.configure_set(["bad", "path"])
+
+ def test_invalid_json_raises(self):
+ from ansible_collections.vyos.rest.plugins.module_utils.vyos_rest import (
+ VyOSRestError,
+ )
+
+ client = _make_client()
+ bad_response = MagicMock()
+ bad_response.read.return_value = b"not json"
+ with patch(
+ "ansible_collections.vyos.rest.plugins.module_utils.vyos_rest.open_url",
+ return_value=bad_response,
+ ):
+ with pytest.raises(VyOSRestError, match="Invalid JSON"):
+ client.retrieve_show_config([])