From 6dc9d721513dca5a573c7ee01b74d703ff41f95f Mon Sep 17 00:00:00 2001 From: omnom62 <75066712+omnom62@users.noreply.github.com> Date: Fri, 21 Aug 2026 23:41:34 +1000 Subject: T8989: prefix_lists module * prefix_lists module * prefix_lists module rst * t8989 prefix_lists module * Converage tests fixes * T8989: SIT updates --- README.md | 13 +- changelogs/fragments/t8989_prefix_lists.yml | 3 + docs/vyos.rest.vyos_prefix_lists_module.rst | 454 +++++++++++++++++++++ plugins/modules/vyos_prefix_lists.py | 357 ++++++++++++++++ .../vyos_prefix_lists/tests/httpapi/_deleted.yaml | 28 -- .../vyos_prefix_lists/tests/httpapi/_gathered.yaml | 20 - .../vyos_prefix_lists/tests/httpapi/_merged.yaml | 42 -- .../vyos_prefix_lists/tests/httpapi/_replaced.yaml | 37 -- .../vyos_prefix_lists/tests/httpapi/deleted.yaml | 28 ++ .../vyos_prefix_lists/tests/httpapi/gathered.yaml | 20 + .../vyos_prefix_lists/tests/httpapi/merged.yaml | 42 ++ .../tests/httpapi/overridden.yaml | 38 ++ .../vyos_prefix_lists/tests/httpapi/replaced.yaml | 37 ++ .../vyos_prefix_lists/tests/httpapi/rtt.yaml | 81 ++++ tests/unit/fixtures/prefix_lists_running.json | 9 +- tests/unit/modules/test_vyos_prefix_lists.py | 222 ++++++++++ 16 files changed, 1290 insertions(+), 141 deletions(-) create mode 100644 changelogs/fragments/t8989_prefix_lists.yml create mode 100644 docs/vyos.rest.vyos_prefix_lists_module.rst create mode 100644 plugins/modules/vyos_prefix_lists.py delete mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/_deleted.yaml delete mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/_gathered.yaml delete mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/_merged.yaml delete mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/overridden.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml create mode 100644 tests/integration/targets/vyos_prefix_lists/tests/httpapi/rtt.yaml create mode 100644 tests/unit/modules/test_vyos_prefix_lists.py 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 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ config + +
+ list + / elements=dictionary +
+
+ +
List of prefix-list configurations grouped by address family.
+
+
+ afi + +
+ string + / required +
+
+
    Choices: +
  • ipv4
  • +
  • ipv6
  • +
+
+
Address family identifier.
+
+
+ prefix_lists + +
+ list + / elements=dictionary +
+
+ +
Named prefix lists.
+
+
+ description + +
+ string +
+
+ +
Prefix list description.
+
+
+ entries + +
+ list + / elements=dictionary +
+
+ +
Prefix list rules.
+
+
+ action + +
+ string +
+
+
    Choices: +
  • permit
  • +
  • deny
  • +
+
+
Permit or deny.
+
+
+ description + +
+ string +
+
+ +
Rule description.
+
+
+ ge + +
+ integer +
+
+ +
Minimum prefix length.
+
+
+ le + +
+ integer +
+
+ +
Maximum prefix length.
+
+
+ prefix + +
+ string +
+
+ +
Network prefix to match.
+
+
+ sequence + +
+ integer + / required +
+
+ +
Rule sequence number.
+
+
+ name + +
+ string + / required +
+
+ +
Prefix list name.
+
+
+ state + +
+ string +
+
+
    Choices: +
  • merged ←
  • +
  • replaced
  • +
  • overridden
  • +
  • deleted
  • +
  • gathered
  • +
+
+
Desired state of the prefix-list configuration.
+
merged adds or updates entries without removing existing ones.
+
replaced replaces each named prefix list mentioned in config.
+
overridden replaces all prefix lists for the given AFIs.
+
deleted removes prefix lists. Without config removes all.
+
gathered returns current configuration as structured data.
+
+
+ + +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 `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KeyReturnedDescription
+
+ after + +
+ list +
+
when changed +
Prefix list configuration after this module ran.
+
+
+
+ before + +
+ list +
+
always +
Prefix list configuration before this module ran.
+
+
+
+ commands + +
+ list +
+
always +
List of API command tuples sent to the device.
+
+
+
+ gathered + +
+ list +
+
when state is gathered +
Current prefix list configuration as structured data.
+
+
+
+ response + +
+ dictionary +
+
when changes are applied +
Raw API response.
+
+
+
+ saved + +
+ boolean +
+
when changes are applied +
Whether the config was saved after changes.
+
+
+

+ + +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 deleted file mode 100644 index 6cfb063..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_deleted.yaml +++ /dev/null @@ -1,28 +0,0 @@ ---- -- debug: - msg: START vyos_prefix_lists deleted integration tests on connection={{ ansible_connection }} - -- include_tasks: _remove_config.yaml -- include_tasks: _populate_config.yaml - -- block: - - name: Delete all prefix_lists configuration - register: result - vyos.rest.vyos_prefix_lists: &id001 - state: deleted - - - assert: - that: - - result.changed == true - - - name: Delete prefix_lists configuration (IDEMPOTENT) - register: result - vyos.rest.vyos_prefix_lists: *id001 - - - name: Assert idempotent - assert: - that: - - result.changed == false - - always: - - include_tasks: _remove_config.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 deleted file mode 100644 index 083cb48..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_gathered.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -- debug: - msg: START vyos_prefix_lists gathered integration tests on connection={{ ansible_connection }} - -- include_tasks: _remove_config.yaml -- include_tasks: _populate_config.yaml - -- block: - - name: Gather prefix_lists configuration - register: result - vyos.rest.vyos_prefix_lists: - state: gathered - - - assert: - that: - - result.changed == false - - result.gathered | selectattr('afi','eq','ipv4') | list | length > 0 - - always: - - include_tasks: _remove_config.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 deleted file mode 100644 index a70f826..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_merged.yaml +++ /dev/null @@ -1,42 +0,0 @@ ---- -- debug: - msg: START vyos_prefix_lists merged integration tests on connection={{ ansible_connection }} - -- include_tasks: _remove_config.yaml - -- block: - - name: Merge prefix_lists configuration - register: result - vyos.rest.vyos_prefix_lists: &id001 - 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 - - - assert: - that: - - result.changed == true - - - name: Merge prefix_lists configuration (IDEMPOTENT) - register: result - vyos.rest.vyos_prefix_lists: *id001 - - - name: Assert idempotent - assert: - that: - - result.changed == false - - result.commands == [] - - always: - - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml deleted file mode 100644 index bd8c5c1..0000000 --- a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/_replaced.yaml +++ /dev/null @@ -1,37 +0,0 @@ ---- -- debug: - msg: START vyos_prefix_lists replaced integration tests on connection={{ ansible_connection }} - -- include_tasks: _remove_config.yaml -- include_tasks: _populate_config.yaml - -- block: - - name: Replace prefix_lists configuration - register: result - vyos.rest.vyos_prefix_lists: &id001 - config: - - afi: ipv4 - prefix_lists: - - name: AnsibleIPv4PrefixList - entries: - - sequence: 10 - action: permit - prefix: 10.0.0.0/8 - state: replaced - - - assert: - that: - - result.changed == true - - - name: Replace prefix_lists configuration (IDEMPOTENT) - register: result - vyos.rest.vyos_prefix_lists: *id001 - - - name: Assert idempotent - assert: - that: - - result.changed == false - - result.commands == [] - - always: - - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..6cfb063 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/deleted.yaml @@ -0,0 +1,28 @@ +--- +- debug: + msg: START vyos_prefix_lists deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete all prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + + always: + - include_tasks: _remove_config.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 new file mode 100644 index 0000000..083cb48 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/gathered.yaml @@ -0,0 +1,20 @@ +--- +- debug: + msg: START vyos_prefix_lists gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: + state: gathered + + - assert: + that: + - result.changed == false + - result.gathered | selectattr('afi','eq','ipv4') | list | length > 0 + + always: + - include_tasks: _remove_config.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 new file mode 100644 index 0000000..a70f826 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/merged.yaml @@ -0,0 +1,42 @@ +--- +- debug: + msg: START vyos_prefix_lists merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + 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 + + - assert: + that: + - result.changed == true + + - name: Merge prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/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 new file mode 100644 index 0000000..bd8c5c1 --- /dev/null +++ b/tests/integration/targets/vyos_prefix_lists/tests/httpapi/replaced.yaml @@ -0,0 +1,37 @@ +--- +- debug: + msg: START vyos_prefix_lists replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace prefix_lists configuration + register: result + vyos.rest.vyos_prefix_lists: &id001 + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + entries: + - sequence: 10 + action: permit + prefix: 10.0.0.0/8 + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace prefix_lists configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_prefix_lists: *id001 + + - name: Assert idempotent + assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_prefix_lists/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() -- cgit v1.2.3