From a0393f9be79bdb45e7a4dcd51b50cdb3d840988c Mon Sep 17 00:00:00 2001 From: omnom62 Date: Mon, 29 Jun 2026 08:52:51 +1000 Subject: T8989: ospf_v3 module --- plugins/modules/vyos_ospfv3.py | 360 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 plugins/modules/vyos_ospfv3.py (limited to 'plugins/modules') 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() -- cgit v1.2.3