summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-08-21 23:41:34 +1000
committerGitHub <noreply@github.com>2026-08-21 08:41:34 -0500
commit6dc9d721513dca5a573c7ee01b74d703ff41f95f (patch)
treeaf2ccddbe4050547ab1f12043f3a4c06c03b4365
parent0fe5f71123e5ff1d489a91486076fc875fa2810e (diff)
downloadrest.vyos-6dc9d721513dca5a573c7ee01b74d703ff41f95f.tar.gz
rest.vyos-6dc9d721513dca5a573c7ee01b74d703ff41f95f.zip
T8989: prefix_lists module
* prefix_lists module * prefix_lists module rst * t8989 prefix_lists module * Converage tests fixes * T8989: SIT updates
-rw-r--r--README.md13
-rw-r--r--changelogs/fragments/t8989_prefix_lists.yml3
-rw-r--r--docs/vyos.rest.vyos_prefix_lists_module.rst454
-rw-r--r--plugins/modules/vyos_prefix_lists.py357
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml (renamed from tests/integration/targets/vyos_prefix_lists/tests/httpapi/_deleted.yaml)0
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml (renamed from tests/integration/targets/vyos_prefix_lists/tests/httpapi/_gathered.yaml)0
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml (renamed from tests/integration/targets/vyos_prefix_lists/tests/httpapi/_merged.yaml)0
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml38
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml (renamed from tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml)0
-rw-r--r--tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml81
-rw-r--r--tests/unit/fixtures/prefix_lists_running.json9
-rw-r--r--tests/unit/modules/test_vyos_prefix_lists.py222
12 files changed, 1163 insertions, 14 deletions
diff --git a/README.md b/README.md
index a9b9b55..647c33a 100644
--- a/README.md
+++ b/README.md
@@ -74,20 +74,17 @@ Name | Description
[vyos.rest.vyos](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_httpapi.rst)|HttpApi plugin for VyOS REST API
### Modules
-
-Modules marked ⚠️ are not yet available in this release.
-
Name | Description
--- | ---
[vyos.rest.vyos_banner](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_banner_module.rst)|Manage multiline banners on VyOS devices via REST API.
[vyos.rest.vyos_configure](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_configure_module.rst)|Send raw set/delete commands to a VyOS device via REST API.
[vyos.rest.vyos_hostname](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_hostname_module.rst)|Manage the system hostname on a VyOS device via the REST API.
[vyos.rest.vyos_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_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API.
-[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API.
-[vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|⚠️ Manage prefix-list configuration on VyOS devices using REST API. *(not yet available)*
-[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API.
-[vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API.
+[vyos.rest.vyos_logging_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_logging_global_module.rst)|Manage syslog configuration on VyOS devices using REST API
+[vyos.rest.vyos_ntp_global](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ntp_global_module.rst)|Manage NTP configuration on VyOS devices using REST API
+[vyos.rest.vyos_prefix_lists](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_prefix_lists_module.rst)|Manage prefix-list configuration on VyOS devices using REST API
+[vyos.rest.vyos_route_maps](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_route_maps_module.rst)|Manage route-map configuration on VyOS devices using REST API
+[vyos.rest.vyos_snmp_server](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_snmp_server_module.rst)|Manage SNMP server configuration on VyOS devices using REST API
<!--end collection content-->
diff --git a/changelogs/fragments/t8989_prefix_lists.yml b/changelogs/fragments/t8989_prefix_lists.yml
new file mode 100644
index 0000000..668a15e
--- /dev/null
+++ b/changelogs/fragments/t8989_prefix_lists.yml
@@ -0,0 +1,3 @@
+---
+trivial:
+ - vyos_prefix_lists - Added new module and tests and documentation.
diff --git a/docs/vyos.rest.vyos_prefix_lists_module.rst b/docs/vyos.rest.vyos_prefix_lists_module.rst
new file mode 100644
index 0000000..53321c9
--- /dev/null
+++ b/docs/vyos.rest.vyos_prefix_lists_module.rst
@@ -0,0 +1,454 @@
+.. _vyos.rest.vyos_prefix_lists_module:
+
+
+***************************
+vyos.rest.vyos_prefix_lists
+***************************
+
+**Manage prefix-list configuration on VyOS devices using REST API**
+
+
+Version added: 1.0.0
+
+.. contents::
+ :local:
+ :depth: 1
+
+
+Synopsis
+--------
+- Manages IPv4 and IPv6 prefix lists on VyOS via the REST API.
+- Uses REST API (``connection=httpapi``) instead of CLI.
+
+
+
+
+Parameters
+----------
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="4">Parameter</th>
+ <th>Choices/<font color="blue">Defaults</font></th>
+ <th width="100%">Comments</th>
+ </tr>
+ <tr>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>config</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>List of prefix-list configurations grouped by address family.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>afi</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>ipv4</li>
+ <li>ipv6</li>
+ </ul>
+ </td>
+ <td>
+ <div>Address family identifier.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td colspan="3">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix_lists</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ / <span style="color: purple">elements=dictionary</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Named prefix lists.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Prefix list description.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>entries</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>Prefix list rules.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>action</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li>permit</li>
+ <li>deny</li>
+ </ul>
+ </td>
+ <td>
+ <div>Permit or deny.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>description</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule description.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>ge</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>Minimum prefix length.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>le</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Maximum prefix length.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>prefix</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>Network prefix to match.</div>
+ </td>
+ </tr>
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>sequence</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">integer</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Rule sequence number.</div>
+ </td>
+ </tr>
+
+ <tr>
+ <td class="elbow-placeholder"></td>
+ <td class="elbow-placeholder"></td>
+ <td colspan="2">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>name</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ / <span style="color: red">required</span>
+ </div>
+ </td>
+ <td>
+ </td>
+ <td>
+ <div>Prefix list name.</div>
+ </td>
+ </tr>
+
+
+ <tr>
+ <td colspan="4">
+ <div class="ansibleOptionAnchor" id="parameter-"></div>
+ <b>state</b>
+ <a class="ansibleOptionLink" href="#parameter-" title="Permalink to this option"></a>
+ <div style="font-size: small">
+ <span style="color: purple">string</span>
+ </div>
+ </td>
+ <td>
+ <ul style="margin: 0; padding: 0"><b>Choices:</b>
+ <li><div style="color: blue"><b>merged</b>&nbsp;&larr;</div></li>
+ <li>replaced</li>
+ <li>overridden</li>
+ <li>deleted</li>
+ <li>gathered</li>
+ </ul>
+ </td>
+ <td>
+ <div>Desired state of the prefix-list configuration.</div>
+ <div><code>merged</code> adds or updates entries without removing existing ones.</div>
+ <div><code>replaced</code> replaces each named prefix list mentioned in config.</div>
+ <div><code>overridden</code> replaces all prefix lists for the given AFIs.</div>
+ <div><code>deleted</code> removes prefix lists. Without config removes all.</div>
+ <div><code>gathered</code> returns current configuration as structured data.</div>
+ </td>
+ </tr>
+ </table>
+ <br/>
+
+
+Notes
+-----
+
+.. note::
+ - Requires ``ansible_connection=httpapi`` with the VyOS httpapi plugin.
+ - ``ansible_network_os`` must be set to ``vyos.rest.vyos``.
+
+
+
+Examples
+--------
+
+.. code-block:: yaml
+
+ - name: Merge prefix list configuration
+ vyos.rest.vyos_prefix_lists:
+ config:
+ - afi: ipv4
+ prefix_lists:
+ - name: AnsibleIPv4PrefixList
+ description: PL configured by ansible
+ entries:
+ - sequence: 2
+ action: permit
+ prefix: 92.168.10.0/26
+ le: 32
+ - sequence: 3
+ action: deny
+ prefix: 72.168.2.0/24
+ ge: 26
+ - afi: ipv6
+ prefix_lists:
+ - name: AllowIPv6Prefix
+ entries:
+ - sequence: 5
+ action: permit
+ prefix: 2001:db8:8000::/35
+ le: 37
+ state: merged
+
+ - name: Delete all prefix lists
+ vyos.rest.vyos_prefix_lists:
+ state: deleted
+
+ - name: Gather current prefix list configuration
+ vyos.rest.vyos_prefix_lists:
+ state: gathered
+
+
+
+Return Values
+-------------
+Common return values are documented `here <https://docs.ansible.com/ansible/latest/reference_appendices/common_return_values.html#common-return-values>`_, the following are the fields unique to this module:
+
+.. raw:: html
+
+ <table border=0 cellpadding=0 class="documentation-table">
+ <tr>
+ <th colspan="1">Key</th>
+ <th>Returned</th>
+ <th width="100%">Description</th>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>after</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when changed</td>
+ <td>
+ <div>Prefix list configuration after this module ran.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>before</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>always</td>
+ <td>
+ <div>Prefix list configuration before this module ran.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>commands</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>always</td>
+ <td>
+ <div>List of API command tuples sent to the device.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>gathered</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">list</span>
+ </div>
+ </td>
+ <td>when state is gathered</td>
+ <td>
+ <div>Current prefix list configuration as structured data.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>response</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">dictionary</span>
+ </div>
+ </td>
+ <td>when changes are applied</td>
+ <td>
+ <div>Raw API response.</div>
+ <br/>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="1">
+ <div class="ansibleOptionAnchor" id="return-"></div>
+ <b>saved</b>
+ <a class="ansibleOptionLink" href="#return-" title="Permalink to this return value"></a>
+ <div style="font-size: small">
+ <span style="color: purple">boolean</span>
+ </div>
+ </td>
+ <td>when changes are applied</td>
+ <td>
+ <div>Whether the config was saved after changes.</div>
+ <br/>
+ </td>
+ </tr>
+ </table>
+ <br/><br/>
+
+
+Status
+------
+
+
+Authors
+~~~~~~~
+
+- VyOS Community (@vyos)
diff --git a/plugins/modules/vyos_prefix_lists.py b/plugins/modules/vyos_prefix_lists.py
new file mode 100644
index 0000000..f3df68f
--- /dev/null
+++ b/plugins/modules/vyos_prefix_lists.py
@@ -0,0 +1,357 @@
+#!/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_prefix_lists
+short_description: Manage prefix-list configuration on VyOS devices using REST API
+description:
+ - Manages IPv4 and IPv6 prefix lists on VyOS via the REST API.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+
+options:
+ config:
+ description: List of prefix-list configurations grouped by address family.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family identifier.
+ 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: Prefix list description.
+ type: str
+ entries:
+ description: Prefix list rules.
+ type: list
+ elements: dict
+ suboptions:
+ sequence:
+ description: Rule sequence number.
+ type: int
+ required: true
+ description:
+ description: Rule 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:
+ description:
+ - Desired state of the prefix-list configuration.
+ - C(merged) adds or updates entries without removing existing ones.
+ - C(replaced) replaces each named prefix list mentioned in config.
+ - C(overridden) replaces all prefix lists for the given AFIs.
+ - C(deleted) removes prefix lists. Without config removes all.
+ - C(gathered) returns current configuration as structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+
+notes:
+ - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin.
+ - C(ansible_network_os) must be set to C(vyos.rest.vyos).
+"""
+
+EXAMPLES = r"""
+- name: Merge prefix list configuration
+ vyos.rest.vyos_prefix_lists:
+ config:
+ - afi: ipv4
+ prefix_lists:
+ - name: AnsibleIPv4PrefixList
+ description: PL configured by ansible
+ entries:
+ - sequence: 2
+ action: permit
+ prefix: 92.168.10.0/26
+ le: 32
+ - sequence: 3
+ action: deny
+ prefix: 72.168.2.0/24
+ ge: 26
+ - afi: ipv6
+ prefix_lists:
+ - name: AllowIPv6Prefix
+ entries:
+ - sequence: 5
+ action: permit
+ prefix: 2001:db8:8000::/35
+ le: 37
+ state: merged
+
+- name: Delete all prefix lists
+ vyos.rest.vyos_prefix_lists:
+ state: deleted
+
+- name: Gather current prefix list configuration
+ vyos.rest.vyos_prefix_lists:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Prefix list configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: Prefix list configuration after this module ran.
+ returned: when changed
+ type: list
+commands:
+ description: List of API command tuples sent to the device.
+ returned: always
+ type: list
+gathered:
+ description: Current prefix list configuration as structured data.
+ returned: when state is gathered
+ type: list
+saved:
+ description: Whether the config was saved after changes.
+ returned: when changes are applied
+ type: bool
+response:
+ description: Raw API response.
+ returned: when changes are applied
+ type: dict
+"""
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule
+
+
+_PL_KEY = {"ipv4": "prefix-list", "ipv6": "prefix-list6"}
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(["policy"])
+ if not raw or not isinstance(raw, dict):
+ return []
+
+ result = []
+ for afi, api_key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]:
+ pl_data = raw.get(api_key) or {}
+ if not isinstance(pl_data, dict):
+ continue
+
+ pls = []
+ for pl_name, pl_info in sorted(pl_data.items()):
+ entry = {"name": pl_name}
+ pl_info = pl_info or {}
+
+ if pl_info.get("description"):
+ entry["description"] = pl_info["description"]
+
+ rules = []
+ for seq, rdata in sorted(
+ (pl_info.get("rule") or {}).items(),
+ key=lambda x: int(x[0]),
+ ):
+ rdata = rdata or {}
+ rule = {"sequence": int(seq)}
+ if rdata.get("action"):
+ rule["action"] = rdata["action"]
+ if rdata.get("prefix"):
+ rule["prefix"] = rdata["prefix"]
+ if rdata.get("ge") is not None:
+ rule["ge"] = int(rdata["ge"])
+ if rdata.get("le") is not None:
+ rule["le"] = int(rdata["le"])
+ if rdata.get("description"):
+ rule["description"] = rdata["description"]
+ rules.append(rule)
+
+ if rules:
+ entry["entries"] = rules
+ pls.append(entry)
+
+ if pls:
+ result.append({"afi": afi, "prefix_lists": pls})
+
+ return result
+
+
+def _normalize(config):
+ result = {"ipv4": {}, "ipv6": {}}
+ for entry in config or []:
+ afi = entry.get("afi")
+ if afi not in result:
+ continue
+ for pl in entry.get("prefix_lists") or []:
+ name = pl["name"]
+ rules = {}
+ for r in pl.get("entries") or []:
+ seq = r["sequence"]
+ rules[seq] = {k: v for k, v in r.items() if k != "sequence" and v is not None}
+ result[afi][name] = {
+ "description": pl.get("description"),
+ "rules": rules,
+ }
+ return result
+
+
+def _rule_cmds(base, seq, want_rule, have_rules, state):
+ cmds = []
+ rbase = base + ["rule", str(seq)]
+
+ if seq not in (have_rules or {}):
+ for field, api_key in [
+ ("action", "action"),
+ ("prefix", "prefix"),
+ ("description", "description"),
+ ]:
+ if want_rule.get(field):
+ cmds.append(("set", rbase + [api_key, want_rule[field]]))
+ if want_rule.get("ge") is not None:
+ cmds.append(("set", rbase + ["ge", str(want_rule["ge"])]))
+ if want_rule.get("le") is not None:
+ cmds.append(("set", rbase + ["le", str(want_rule["le"])]))
+ else:
+ h = have_rules[seq]
+ for field, api_key in [
+ ("action", "action"),
+ ("prefix", "prefix"),
+ ("description", "description"),
+ ]:
+ if want_rule.get(field) and want_rule[field] != h.get(field):
+ cmds.append(("set", rbase + [api_key, want_rule[field]]))
+ for field in ("ge", "le"):
+ if want_rule.get(field) is not None and want_rule[field] != h.get(field):
+ cmds.append(("set", rbase + [field, str(want_rule[field])]))
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+
+ if state == "deleted":
+ if not config:
+ for afi, api_key in _PL_KEY.items():
+ if any(e.get("afi") == afi for e in have_raw):
+ cmds.append(("delete", ["policy", api_key]))
+ else:
+ for entry in config:
+ afi = entry["afi"]
+ api_key = _PL_KEY[afi]
+ for pl in entry.get("prefix_lists") or []:
+ cmds.append(("delete", ["policy", api_key, pl["name"]]))
+ return cmds
+
+ want = _normalize(config)
+ have = _normalize(have_raw)
+
+ for afi, api_key in _PL_KEY.items():
+ want_afi = want.get(afi, {})
+ have_afi = have.get(afi, {})
+
+ if state == "overridden":
+ for pl_name in set(have_afi) - set(want_afi):
+ cmds.append(("delete", ["policy", api_key, pl_name]))
+
+ for pl_name, want_pl in want_afi.items():
+ have_pl = have_afi.get(pl_name, {})
+ base = ["policy", api_key, pl_name]
+ want_rules = want_pl.get("rules", {})
+ have_rules = have_pl.get("rules", {})
+
+ if state == "replaced" and pl_name in have_afi:
+ # Only delete and rebuild if something actually differs
+ want_desc = want_pl.get("description")
+ have_desc = have_pl.get("description")
+ extra_seqs = set(have_rules.keys()) - set(want_rules.keys())
+ test_cmds = []
+ for seq, want_rule in want_rules.items():
+ test_cmds += _rule_cmds(base, seq, want_rule, have_rules, state)
+ if test_cmds or extra_seqs or want_desc != have_desc:
+ cmds.append(("delete", ["policy", api_key, pl_name]))
+ have_pl = {}
+ have_rules = {}
+ else:
+ continue # already matches — idempotent
+
+ # Description
+ if want_pl.get("description") and want_pl["description"] != have_pl.get("description"):
+ cmds.append(("set", base + ["description", want_pl["description"]]))
+
+ # Rules
+ for seq, want_rule in want_rules.items():
+ cmds += _rule_cmds(base, seq, want_rule, have_rules, state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(type="list", elements="dict"),
+ state=dict(
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True)
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ have = get_running_config(vyos)
+
+ if state == "gathered":
+ module.exit_json(changed=False, gathered=have)
+
+ commands = build_commands(config, have, state)
+
+ if module.check_mode:
+ module.exit_json(changed=bool(commands), commands=commands, before=have)
+
+ if commands:
+ response = vyos.apply_commands(commands)
+ saved = vyos.save_config()
+ module.exit_json(
+ changed=True,
+ before=have,
+ after=get_running_config(vyos),
+ commands=commands,
+ saved=saved,
+ response=response,
+ )
+
+ module.exit_json(changed=False, before=have, after=have, commands=[])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_deleted.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml
index 6cfb063..6cfb063 100644
--- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_deleted.yaml
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_gathered.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml
index 083cb48..083cb48 100644
--- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_gathered.yaml
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_merged.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml
index a70f826..a70f826 100644
--- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_merged.yaml
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml
new file mode 100644
index 0000000..f895488
--- /dev/null
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml
@@ -0,0 +1,38 @@
+---
+- debug:
+ msg: START vyos_prefix_lists overridden integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+- include_tasks: _populate_config.yaml
+
+- block:
+ - name: Override prefix_lists configuration
+ register: result
+ vyos.rest.vyos_prefix_lists: &id001
+ config:
+ - afi: ipv4
+ prefix_lists:
+ - name: AnsibleIPv4PrefixList
+ description: Overridden by ansible
+ entries:
+ - sequence: 2
+ action: permit
+ prefix: 92.168.10.0/26
+ le: 32
+ state: overridden
+
+ - assert:
+ that:
+ - result.changed == true
+
+ - name: Override prefix_lists configuration (IDEMPOTENT)
+ register: result
+ vyos.rest.vyos_prefix_lists: *id001
+
+ - assert:
+ that:
+ - result.changed == false
+ - result.commands == []
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml
index bd8c5c1..bd8c5c1 100644
--- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml
diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml
new file mode 100644
index 0000000..de816eb
--- /dev/null
+++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml
@@ -0,0 +1,81 @@
+---
+- debug:
+ msg: START vyos_prefix_lists round trip integration tests on connection={{ ansible_connection }}
+
+- include_tasks: _remove_config.yaml
+
+- block:
+ - name: RTT - Apply base configuration
+ vyos.rest.vyos_prefix_lists:
+ config:
+ - afi: ipv4
+ prefix_lists:
+ - name: AnsibleIPv4PrefixList
+ description: PL configured by ansible
+ entries:
+ - sequence: 2
+ action: permit
+ prefix: 92.168.10.0/26
+ le: 32
+ - sequence: 3
+ action: deny
+ prefix: 72.168.2.0/24
+ ge: 26
+ state: merged
+
+ - name: RTT - Gather configuration
+ register: gathered
+ vyos.rest.vyos_prefix_lists:
+ state: gathered
+
+ - name: RTT - Assert gathered matches applied
+ vars:
+ ipv4_pls: >-
+ {{ gathered.gathered
+ | selectattr('afi', 'eq', 'ipv4')
+ | map(attribute='prefix_lists')
+ | first }}
+ assert:
+ that:
+ - gathered.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1
+ - ipv4_pls | selectattr('name', 'eq', 'AnsibleIPv4PrefixList') | list | length == 1
+
+ - name: RTT - Modify configuration
+ vyos.rest.vyos_prefix_lists:
+ config:
+ - afi: ipv4
+ prefix_lists:
+ - name: AnsibleIPv4PrefixList
+ description: Modified by ansible
+ entries:
+ - sequence: 2
+ action: permit
+ prefix: 92.168.10.0/26
+ le: 32
+ state: replaced
+
+ - name: RTT - Gather modified configuration
+ register: gathered2
+ vyos.rest.vyos_prefix_lists:
+ state: gathered
+
+ - name: RTT - Assert modification applied correctly
+ vars:
+ ipv4_pls2: >-
+ {{ gathered2.gathered
+ | selectattr('afi', 'eq', 'ipv4')
+ | map(attribute='prefix_lists')
+ | first }}
+ pl_entries: >-
+ {{ ipv4_pls2
+ | selectattr('name', 'eq', 'AnsibleIPv4PrefixList')
+ | map(attribute='entries')
+ | first }}
+ assert:
+ that:
+ - gathered2.gathered | selectattr('afi', 'eq', 'ipv4') | list | length == 1
+ - pl_entries | length == 1
+ - pl_entries | selectattr('sequence', 'eq', 2) | list | length == 1
+
+ always:
+ - include_tasks: _remove_config.yaml
diff --git a/tests/unit/fixtures/prefix_lists_running.json b/tests/unit/fixtures/prefix_lists_running.json
index 2a6ecd3..ebc72d0 100644
--- a/tests/unit/fixtures/prefix_lists_running.json
+++ b/tests/unit/fixtures/prefix_lists_running.json
@@ -6,14 +6,12 @@
"2": {
"action": "permit",
"prefix": "92.168.10.0/26",
- "le": "32",
- "description": "Rule 2 given by ansible"
+ "le": "32"
},
"3": {
"action": "deny",
"prefix": "72.168.2.0/24",
- "ge": "26",
- "description": "Rule 3"
+ "ge": "26"
}
}
}
@@ -25,8 +23,7 @@
"5": {
"action": "permit",
"prefix": "2001:db8:8000::/35",
- "le": "37",
- "description": "Permit rule"
+ "le": "37"
}
}
},
diff --git a/tests/unit/modules/test_vyos_prefix_lists.py b/tests/unit/modules/test_vyos_prefix_lists.py
new file mode 100644
index 0000000..e33de42
--- /dev/null
+++ b/tests/unit/modules/test_vyos_prefix_lists.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+import json
+import os
+import unittest
+
+from unittest.mock import MagicMock
+
+from ansible_collections.vyos.rest.plugins.modules.vyos_prefix_lists import (
+ _normalize,
+ build_commands,
+ get_running_config,
+)
+
+
+def load_fixture(filename):
+ fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures")
+ path = os.path.join(fixtures_dir, filename)
+ with open(path) as f:
+ return json.load(f)
+
+
+class VyOSModuleTestCase(unittest.TestCase):
+ def setUp(self):
+ self.mock_vyos = MagicMock()
+ self.mock_vyos.get_config = MagicMock(return_value={})
+
+ def set_running_config(self, data):
+ self.mock_vyos.get_config.return_value = data
+
+
+class TestVyOSPrefixListsGetRunning(VyOSModuleTestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.fixture = load_fixture("prefix_lists_running.json")
+
+ def test_parses_ipv4_prefix_list(self):
+ self.set_running_config(self.fixture)
+ result = get_running_config(self.mock_vyos)
+ ipv4 = next((e for e in result if e["afi"] == "ipv4"), None)
+ self.assertIsNotNone(ipv4)
+ pl = next((p for p in ipv4["prefix_lists"] if p["name"] == "AnsibleIPv4PrefixList"), None)
+ self.assertIsNotNone(pl)
+ self.assertEqual(pl["description"], "PL configured by ansible")
+
+ def test_parses_ipv4_rules(self):
+ self.set_running_config(self.fixture)
+ result = get_running_config(self.mock_vyos)
+ ipv4 = next(e for e in result if e["afi"] == "ipv4")
+ pl = ipv4["prefix_lists"][0]
+ seqs = [r["sequence"] for r in pl["entries"]]
+ self.assertIn(2, seqs)
+ self.assertIn(3, seqs)
+
+ def test_parses_ipv4_rule_fields(self):
+ self.set_running_config(self.fixture)
+ result = get_running_config(self.mock_vyos)
+ ipv4 = next(e for e in result if e["afi"] == "ipv4")
+ rule2 = next(r for r in ipv4["prefix_lists"][0]["entries"] if r["sequence"] == 2)
+ self.assertEqual(rule2["action"], "permit")
+ self.assertEqual(rule2["prefix"], "92.168.10.0/26")
+ self.assertEqual(rule2["le"], 32)
+
+ def test_parses_ipv6_prefix_lists(self):
+ self.set_running_config(self.fixture)
+ result = get_running_config(self.mock_vyos)
+ ipv6 = next((e for e in result if e["afi"] == "ipv6"), None)
+ self.assertIsNotNone(ipv6)
+ names = [p["name"] for p in ipv6["prefix_lists"]]
+ self.assertIn("AllowIPv6Prefix", names)
+ self.assertIn("DenyIPv6Prefix", names)
+
+ def test_empty_returns_empty_list(self):
+ self.set_running_config({})
+ result = get_running_config(self.mock_vyos)
+ self.assertEqual(result, [])
+
+
+class TestVyOSPrefixListsNormalize(unittest.TestCase):
+
+ def test_normalize_ipv4(self):
+ config = [
+ {
+ "afi": "ipv4",
+ "prefix_lists": [
+ {
+ "name": "PL1",
+ "entries": [{"sequence": 10, "action": "permit", "prefix": "10.0.0.0/8"}],
+ },
+ ],
+ },
+ ]
+ result = _normalize(config)
+ self.assertIn("PL1", result["ipv4"])
+ self.assertIn(10, result["ipv4"]["PL1"]["rules"])
+ self.assertEqual(result["ipv4"]["PL1"]["rules"][10]["action"], "permit")
+
+ def test_normalize_filters_none_values(self):
+ config = [
+ {
+ "afi": "ipv4",
+ "prefix_lists": [
+ {
+ "name": "PL1",
+ "entries": [
+ {
+ "sequence": 10,
+ "action": "permit",
+ "prefix": "10.0.0.0/8",
+ "ge": None,
+ "le": None,
+ },
+ ],
+ },
+ ],
+ },
+ ]
+ result = _normalize(config)
+ rule = result["ipv4"]["PL1"]["rules"][10]
+ self.assertNotIn("ge", rule)
+ self.assertNotIn("le", rule)
+
+
+class TestVyOSPrefixListsBuildCommands(unittest.TestCase):
+
+ def _have_empty(self):
+ return []
+
+ def _have_with_ipv4_pl(self):
+ return [
+ {
+ "afi": "ipv4",
+ "prefix_lists": [
+ {
+ "name": "PL1",
+ "entries": [
+ {
+ "sequence": 10,
+ "action": "permit",
+ "prefix": "10.0.0.0/8",
+ },
+ ],
+ },
+ ],
+ },
+ ]
+
+ def test_merged_adds_new_prefix_list(self):
+ config = [
+ {
+ "afi": "ipv4",
+ "prefix_lists": [
+ {
+ "name": "PL-NEW",
+ "entries": [
+ {
+ "sequence": 5,
+ "action": "permit",
+ "prefix": "192.168.0.0/24",
+ },
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, self._have_empty(), "merged")
+ paths = [c[1] for c in cmds]
+ self.assertIn(["policy", "prefix-list", "PL-NEW", "rule", "5", "action", "permit"], paths)
+ self.assertIn(
+ ["policy", "prefix-list", "PL-NEW", "rule", "5", "prefix", "192.168.0.0/24"],
+ paths,
+ )
+
+ def test_merged_idempotent_existing_rule(self):
+ config = self._have_with_ipv4_pl()
+ cmds = build_commands(config, self._have_with_ipv4_pl(), "merged")
+ self.assertEqual(cmds, [])
+
+ def test_deleted_no_config_deletes_all(self):
+ cmds = build_commands([], self._have_with_ipv4_pl(), "deleted")
+ self.assertIn(("delete", ["policy", "prefix-list"]), cmds)
+
+ def test_deleted_with_config_deletes_named(self):
+ config = [{"afi": "ipv4", "prefix_lists": [{"name": "PL1"}]}]
+ cmds = build_commands(config, self._have_with_ipv4_pl(), "deleted")
+ self.assertIn(("delete", ["policy", "prefix-list", "PL1"]), cmds)
+
+ def test_replaced_deletes_then_resets(self):
+ config = [
+ {
+ "afi": "ipv4",
+ "prefix_lists": [
+ {
+ "name": "PL1",
+ "entries": [
+ {
+ "sequence": 10,
+ "action": "deny",
+ "prefix": "10.0.0.0/8",
+ },
+ ],
+ },
+ ],
+ },
+ ]
+ cmds = build_commands(config, self._have_with_ipv4_pl(), "replaced")
+ # Should delete PL1 first then re-add
+ ops = [(c[0], c[1]) for c in cmds]
+ delete_idx = next(
+ i for i, c in enumerate(ops) if c == ("delete", ["policy", "prefix-list", "PL1"])
+ )
+ set_idx = next(i for i, c in enumerate(ops) if c[0] == "set" and "deny" in c[1])
+ self.assertLess(delete_idx, set_idx)
+
+
+if __name__ == "__main__":
+ unittest.main()