diff options
| author | omnom62 <omnom62@outlook.com> | 2026-06-29 08:52:51 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-06-29 08:52:51 +1000 |
| commit | a0393f9be79bdb45e7a4dcd51b50cdb3d840988c (patch) | |
| tree | c9679c09d6a6b163c01fe4c049fd7a3d8bc6fe28 | |
| parent | 6be14447461593f2c71088b54fe523cb468e32dd (diff) | |
| download | rest.vyos-a0393f9be79bdb45e7a4dcd51b50cdb3d840988c.tar.gz rest.vyos-a0393f9be79bdb45e7a4dcd51b50cdb3d840988c.zip | |
T8989: ospf_v3 module
17 files changed, 802 insertions, 0 deletions
@@ -86,6 +86,7 @@ Name | Description [vyos.rest.vyos_lldp_interfaces](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_lldp_interfaces_module.rst)|Manage LLDP interface configuration on VyOS devices 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_ospfv3](https://github.com/vyos/vyos.rest/blob/main/docs/vyos.rest.vyos_ospfv3_module.rst)|Manage OSPFv3 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/plugins/modules/vyos_ospfv3.py b/plugins/modules/vyos_ospfv3.py new file mode 100644 index 0000000..fcf404e --- /dev/null +++ b/plugins/modules/vyos_ospfv3.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_ospfv3 +short_description: Manage OSPFv3 configuration on VyOS devices using REST API +description: + - Manages OSPFv3 configuration on VyOS devices 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: OSPFv3 configuration. + type: dict + suboptions: + areas: + description: OSPFv3 areas. + type: list + elements: dict + suboptions: + area_id: + description: Area identity. + type: str + required: true + export_list: + description: Name of export-list. + type: str + import_list: + description: Name of import-list. + type: str + range: + description: Summarize routes matching prefix. + type: list + elements: dict + suboptions: + address: + description: IPv6 prefix. + type: str + required: true + advertise: + description: Advertise this range. + type: bool + not_advertise: + description: Do not advertise this range. + type: bool + parameters: + description: OSPFv3 global parameters. + type: dict + suboptions: + router_id: + description: Router ID (IPv4 address format). + type: str + redistribute: + description: Redistribute routes from another protocol. + type: list + elements: dict + suboptions: + route_type: + description: Protocol to redistribute. + type: str + choices: [bgp, connected, kernel, ripng, static] + route_map: + description: Route map to apply. + type: str + state: + description: + - Desired state of the OSPFv3 configuration. + - C(merged) adds or updates without removing existing config. + - C(replaced) replaces the entire OSPFv3 configuration. + - C(deleted) removes OSPFv3 configuration. + - C(gathered) returns current configuration as structured data. + type: str + choices: [merged, replaced, deleted, gathered] + default: merged +notes: + - Requires C(ansible_connection=httpapi) with the VyOS httpapi plugin. + - C(ansible_network_os) must be set to C(vyos.rest.vyos). +""" + +EXAMPLES = r""" +- name: Merge OSPFv3 configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + import_list: import1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + +- name: Delete all OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: deleted + +- name: Gather current OSPFv3 configuration + vyos.rest.vyos_ospfv3: + state: gathered +""" + +RETURN = r""" +before: + description: OSPFv3 configuration before this module ran. + returned: always + type: dict +after: + description: OSPFv3 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 OSPFv3 configuration as structured data. + returned: when state is gathered + type: dict +saved: + description: Whether the config was saved after changes. + returned: when changes are applied + type: bool +response: + description: Raw API response. + returned: when changes are applied + type: dict +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule + + +_BASE = ["protocols", "ospfv3"] + + +def get_running_config(vyos): + raw = vyos.get_config(_BASE) + if not raw or not isinstance(raw, dict): + return {} + return _parse_ospfv3(raw) + + +def _parse_ospfv3(raw): + result = {} + + # parameters + params = raw.get("parameters", {}) + if params: + result["parameters"] = {} + if "router-id" in params: + result["parameters"]["router_id"] = params["router-id"] + + # redistribute + redist_raw = raw.get("redistribute", {}) + if redist_raw and isinstance(redist_raw, dict): + redist = [] + for route_type, data in sorted(redist_raw.items()): + entry = {"route_type": route_type} + if isinstance(data, dict) and data.get("route-map"): + entry["route_map"] = data["route-map"] + redist.append(entry) + if redist: + result["redistribute"] = redist + + # areas + area_raw = raw.get("area", {}) + if area_raw and isinstance(area_raw, dict): + areas = [] + for area_id, area_data in sorted(area_raw.items()): + area = {"area_id": area_id} + area_data = area_data or {} + if area_data.get("export-list"): + area["export_list"] = area_data["export-list"] + if area_data.get("import-list"): + area["import_list"] = area_data["import-list"] + range_raw = area_data.get("range", {}) + if range_raw and isinstance(range_raw, dict): + ranges = [] + for prefix, rdata in sorted(range_raw.items()): + r = {"address": prefix} + rdata = rdata or {} + if "advertise" in rdata: + r["advertise"] = True + if "not-advertise" in rdata: + r["not_advertise"] = True + ranges.append(r) + if ranges: + area["range"] = ranges + areas.append(area) + if areas: + result["areas"] = areas + + return result + + +def build_commands(config, have, state): + cmds = [] + + if state == "deleted": + if have: + cmds.append(("delete", _BASE)) + return cmds + + if state == "replaced": + # Build what we would set from scratch and compare to have + would_set = build_commands(config, {}, "merged") + have_set = build_commands(have, {}, "merged") + if would_set == have_set: + return [] + if have: + cmds.append(("delete", _BASE)) + have = {} + + # parameters + want_params = (config or {}).get("parameters") or {} + have_params = have.get("parameters") or {} + if want_params.get("router_id") and want_params["router_id"] != have_params.get("router_id"): + cmds.append(("set", _BASE + ["parameters", "router-id", want_params["router_id"]])) + + # redistribute + want_redist = {r["route_type"]: r for r in ((config or {}).get("redistribute") or [])} + have_redist = {r["route_type"]: r for r in (have.get("redistribute") or [])} + + for rt in set(have_redist) - set(want_redist): + if state == "merged": + pass # merged doesn't remove + for rt, entry in want_redist.items(): + if rt not in have_redist: + cmds.append(("set", _BASE + ["redistribute", rt])) + if entry.get("route_map"): + have_rm = have_redist.get(rt, {}).get("route_map") + if entry["route_map"] != have_rm: + cmds.append(("set", _BASE + ["redistribute", rt, "route-map", entry["route_map"]])) + + # areas + want_areas = {a["area_id"]: a for a in ((config or {}).get("areas") or [])} + have_areas = {a["area_id"]: a for a in (have.get("areas") or [])} + + for area_id, want_area in want_areas.items(): + have_area = have_areas.get(area_id, {}) + abase = _BASE + ["area", area_id] + + if want_area.get("export_list") and want_area["export_list"] != have_area.get( + "export_list", + ): + cmds.append(("set", abase + ["export-list", want_area["export_list"]])) + if want_area.get("import_list") and want_area["import_list"] != have_area.get( + "import_list", + ): + cmds.append(("set", abase + ["import-list", want_area["import_list"]])) + + want_ranges = {r["address"]: r for r in (want_area.get("range") or [])} + have_ranges = {r["address"]: r for r in (have_area.get("range") or [])} + + for addr in want_ranges: + if addr not in have_ranges: + cmds.append(("set", abase + ["range", addr])) + r = want_ranges[addr] + if r.get("not_advertise"): + cmds.append(("set", abase + ["range", addr, "not-advertise"])) + elif r.get("advertise"): + cmds.append(("set", abase + ["range", addr, "advertise"])) + + return cmds + + +ARGUMENT_SPEC = dict( + config=dict( + type="dict", + options=dict( + areas=dict( + type="list", + elements="dict", + options=dict( + area_id=dict(type="str", required=True), + export_list=dict(type="str"), + import_list=dict(type="str"), + range=dict( + type="list", + elements="dict", + options=dict( + address=dict(type="str", required=True), + 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( + default="merged", + choices=["merged", "replaced", "deleted", "gathered"], + ), +) + + +def main(): + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + vyos = VyOSModule(module) + + state = module.params["state"] + config = module.params.get("config") or {} + + have = get_running_config(vyos) + + if state == "gathered": + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + + if module.check_mode: + module.exit_json(changed=bool(commands), commands=commands, before=have) + + if commands: + response = vyos.apply_commands(commands) + saved = vyos.save_config() + module.exit_json( + changed=True, + before=have, + after=get_running_config(vyos), + commands=commands, + saved=saved, + response=response, + ) + + module.exit_json(changed=False, before=have, after=have, commands=[]) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/vyos_ospfv3/aliases b/tests/integration/targets/vyos_ospfv3/aliases new file mode 100644 index 0000000..cc0afef --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/aliases @@ -0,0 +1 @@ +network/vyos diff --git a/tests/integration/targets/vyos_ospfv3/defaults/main.yaml b/tests/integration/targets/vyos_ospfv3/defaults/main.yaml new file mode 100644 index 0000000..164afea --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/defaults/main.yaml @@ -0,0 +1,3 @@ +--- +testcase: "[^_].*" +test_items: [] diff --git a/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml b/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml new file mode 100644 index 0000000..4147e6d --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tasks/httpapi.yaml @@ -0,0 +1,21 @@ +--- +- name: Collect all httpapi test cases + ansible.builtin.find: + paths: "{{ role_path }}/tests/httpapi" + patterns: "{{ testcase }}.yaml" + use_regex: true + register: test_cases + delegate_to: localhost + +- name: Set test_items + ansible.builtin.set_fact: + test_items: "{{ test_cases.files | map(attribute='path') | list }}" + +- name: Run test case (connection=httpapi) + ansible.builtin.include_tasks: "{{ test_case_to_run }}" + vars: + ansible_connection: ansible.netcommon.httpapi + ansible_network_os: vyos.rest.vyos + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/tests/integration/targets/vyos_ospfv3/tasks/main.yaml b/tests/integration/targets/vyos_ospfv3/tasks/main.yaml new file mode 100644 index 0000000..b1f6193 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tasks/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Run httpapi tests + ansible.builtin.include_tasks: httpapi.yaml + tags: + - httpapi diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml new file mode 100644 index 0000000..19a61c7 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_populate_config.yaml @@ -0,0 +1,17 @@ +--- +- name: Populate ospfv3 config for testing + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + import_list: import1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml new file mode 100644 index 0000000..fe60eaf --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/_remove_config.yaml @@ -0,0 +1,5 @@ +--- +- name: Remove pre-existing ospfv3 config + vyos.rest.vyos_ospfv3: + state: deleted + ignore_errors: true diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml new file mode 100644 index 0000000..9b09ccd --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/deleted.yaml @@ -0,0 +1,29 @@ +--- +- debug: + msg: START vyos_ospfv3 deleted integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Delete OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + state: deleted + + - assert: + that: + - result.changed == true + + - name: Delete OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: + state: deleted + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml new file mode 100644 index 0000000..bf2cc0c --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/gathered.yaml @@ -0,0 +1,21 @@ +--- +- debug: + msg: START vyos_ospfv3 gathered integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Gather OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + state: gathered + + - assert: + that: + - result.gathered.parameters.router_id == "192.0.2.10" + - result.gathered.redistribute | selectattr('route_type', 'eq', 'bgp') | list | length == 1 + - result.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml new file mode 100644 index 0000000..10702e4 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/merged.yaml @@ -0,0 +1,48 @@ +--- +- debug: + msg: START vyos_ospfv3 merged integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: Merge OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: merged + + - assert: + that: + - result.changed == true + + - name: Merge OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: merged + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml new file mode 100644 index 0000000..609e7ff --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/overridden.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_ospfv3 overridden integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Override OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: &id001 + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "3" + range: + - address: "2001:db40::/32" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Override OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml new file mode 100644 index 0000000..3f1be83 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/replaced.yaml @@ -0,0 +1,35 @@ +--- +- debug: + msg: START vyos_ospfv3 replaced integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml +- include_tasks: _populate_config.yaml + +- block: + - name: Replace OSPFv3 configuration + register: result + vyos.rest.vyos_ospfv3: &id001 + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "3" + range: + - address: "2001:db40::/32" + state: replaced + + - assert: + that: + - result.changed == true + + - name: Replace OSPFv3 configuration (IDEMPOTENT) + register: result + vyos.rest.vyos_ospfv3: *id001 + + - assert: + that: + - result.changed == false + - result.commands == [] + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml b/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml new file mode 100644 index 0000000..7636c23 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/tests/httpapi/rtt.yaml @@ -0,0 +1,60 @@ +--- +- debug: + msg: START vyos_ospfv3 round trip integration tests on connection={{ ansible_connection }} + +- include_tasks: _remove_config.yaml + +- block: + - name: RTT - Apply base configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.10 + redistribute: + - route_type: bgp + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + - address: "2001:db20::/32" + state: merged + + - name: RTT - Gather configuration + register: gathered + vyos.rest.vyos_ospfv3: + state: gathered + + - name: RTT - Assert gathered matches applied + assert: + that: + - gathered.gathered.parameters.router_id == "192.0.2.10" + - "'bgp' in gathered.gathered.redistribute | map(attribute='route_type') | list" + - gathered.gathered.areas | selectattr('area_id', 'eq', '2') | list | length == 1 + + - name: RTT - Modify configuration + vyos.rest.vyos_ospfv3: + config: + parameters: + router_id: 192.0.2.11 + areas: + - area_id: "2" + export_list: export1 + range: + - address: "2001:db10::/32" + state: replaced + + - name: RTT - Gather modified configuration + register: gathered2 + vyos.rest.vyos_ospfv3: + state: gathered + + - name: RTT - Assert modification applied correctly + assert: + that: + - gathered2.gathered.parameters.router_id == "192.0.2.11" + - gathered2.gathered.redistribute is not defined or gathered2.gathered.redistribute | length == 0 + - gathered2.gathered.areas | selectattr('area_id', 'eq', '2') | map(attribute='range') | first | length == 1 + + always: + - include_tasks: _remove_config.yaml diff --git a/tests/integration/targets/vyos_ospfv3/vars/main.yaml b/tests/integration/targets/vyos_ospfv3/vars/main.yaml new file mode 100644 index 0000000..4303881 --- /dev/null +++ b/tests/integration/targets/vyos_ospfv3/vars/main.yaml @@ -0,0 +1,2 @@ +--- +# only common vars here diff --git a/tests/unit/fixtures/ospfv3_running.json b/tests/unit/fixtures/ospfv3_running.json new file mode 100644 index 0000000..44a1bcd --- /dev/null +++ b/tests/unit/fixtures/ospfv3_running.json @@ -0,0 +1,19 @@ +{ + "parameters": { "router-id": "192.0.2.10" }, + "redistribute": { "bgp": {}, "connected": { "route-map": "RM1" } }, + "area": { + "2": { + "export-list": "export1", + "import-list": "import1", + "range": { + "2001:db10::/32": {}, + "2001:db20::/32": { "not-advertise": {} } + } + }, + "3": { + "range": { + "2001:db40::/32": {} + } + } + } +} diff --git a/tests/unit/modules/test_vyos_ospfv3.py b/tests/unit/modules/test_vyos_ospfv3.py new file mode 100644 index 0000000..a84041b --- /dev/null +++ b/tests/unit/modules/test_vyos_ospfv3.py @@ -0,0 +1,140 @@ +# -*- 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_ospfv3 import ( + build_commands, + get_running_config, +) + + +def load_fixture(filename): + fixtures_dir = os.path.join(os.path.dirname(__file__), "..", "fixtures") + with open(os.path.join(fixtures_dir, filename)) as f: + return json.load(f) + + +class VyOSModuleTestCase(unittest.TestCase): + def setUp(self): + self.mock_vyos = MagicMock() + self.mock_vyos.get_config = MagicMock(return_value={}) + + def set_running_config(self, data): + self.mock_vyos.get_config.return_value = data + + +class TestVyOSOspfv3Parse(VyOSModuleTestCase): + + def setUp(self): + super().setUp() + self.fixture = load_fixture("ospfv3_running.json") + + def test_parses_parameters(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertEqual(result["parameters"]["router_id"], "192.0.2.10") + + def test_parses_redistribute(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + route_types = [r["route_type"] for r in result["redistribute"]] + self.assertIn("bgp", route_types) + self.assertIn("connected", route_types) + connected = next(r for r in result["redistribute"] if r["route_type"] == "connected") + self.assertEqual(connected["route_map"], "RM1") + + def test_parses_areas(self): + self.set_running_config(self.fixture) + result = get_running_config(self.mock_vyos) + self.assertEqual(len(result["areas"]), 2) + area2 = next(a for a in result["areas"] if a["area_id"] == "2") + self.assertEqual(area2["export_list"], "export1") + self.assertEqual(area2["import_list"], "import1") + self.assertEqual(len(area2["range"]), 2) + not_adv = next(r for r in area2["range"] if r["address"] == "2001:db20::/32") + self.assertTrue(not_adv["not_advertise"]) + + def test_empty_config_returns_empty(self): + self.set_running_config({}) + result = get_running_config(self.mock_vyos) + self.assertEqual(result, {}) + + +class TestVyOSOspfv3BuildCommands(unittest.TestCase): + + def test_deleted_with_have(self): + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands({}, have, "deleted") + self.assertEqual(cmds, [("delete", ["protocols", "ospfv3"])]) + + def test_deleted_without_have(self): + cmds = build_commands({}, {}, "deleted") + self.assertEqual(cmds, []) + + def test_merged_parameters(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "parameters", "router-id", "192.0.2.10"]), + cmds, + ) + + def test_merged_redistribute(self): + config = {"redistribute": [{"route_type": "bgp"}]} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "redistribute", "bgp"]), + cmds, + ) + + def test_merged_idempotent(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "merged") + self.assertEqual(cmds, []) + + def test_merged_area_range(self): + config = { + "areas": [{"area_id": "2", "range": [{"address": "2001:db10::/32"}]}], + } + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "area", "2", "range", "2001:db10::/32"]), + cmds, + ) + + def test_replaced_idempotent(self): + config = {"parameters": {"router_id": "192.0.2.10"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds, []) + + def test_replaced_rebuilds_on_change(self): + config = {"parameters": {"router_id": "192.0.2.11"}} + have = {"parameters": {"router_id": "192.0.2.10"}} + cmds = build_commands(config, have, "replaced") + self.assertEqual(cmds[0], ("delete", ["protocols", "ospfv3"])) + self.assertIn( + ("set", ["protocols", "ospfv3", "parameters", "router-id", "192.0.2.11"]), + cmds, + ) + + def test_merged_area_export_list(self): + config = {"areas": [{"area_id": "2", "export_list": "export1"}]} + cmds = build_commands(config, {}, "merged") + self.assertIn( + ("set", ["protocols", "ospfv3", "area", "2", "export-list", "export1"]), + cmds, + ) + + +if __name__ == "__main__": + unittest.main() |
