summaryrefslogtreecommitdiff
path: root/plugins/modules/vyos_ospf_interfaces.py
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/modules/vyos_ospf_interfaces.py')
-rw-r--r--plugins/modules/vyos_ospf_interfaces.py539
1 files changed, 539 insertions, 0 deletions
diff --git a/plugins/modules/vyos_ospf_interfaces.py b/plugins/modules/vyos_ospf_interfaces.py
new file mode 100644
index 0000000..5388ef0
--- /dev/null
+++ b/plugins/modules/vyos_ospf_interfaces.py
@@ -0,0 +1,539 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# GNU General Public License v3.0+
+from __future__ import absolute_import, division, print_function
+
+
+__metaclass__ = type
+
+DOCUMENTATION = r"""
+---
+module: vyos_ospf_interfaces
+short_description: Manage OSPF interface configuration on VyOS devices using REST API
+description:
+ - Manages OSPF and OSPFv3 interface configuration on VyOS devices via the REST API.
+ - IPv4 OSPF maps to C(protocols ospf interface).
+ - IPv6 OSPFv3 maps to C(protocols ospfv3 interface).
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of OSPF interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Interface name.
+ type: str
+ required: true
+ address_family:
+ description: OSPF settings per address family.
+ type: list
+ elements: dict
+ suboptions:
+ afi:
+ description: Address family identifier.
+ type: str
+ choices: [ipv4, ipv6]
+ required: true
+ authentication:
+ description: Authentication settings (IPv4 only).
+ type: dict
+ suboptions:
+ plaintext_password:
+ description: Plaintext password.
+ type: str
+ md5_key:
+ description: MD5 authentication key.
+ type: dict
+ suboptions:
+ key_id:
+ description: MD5 key ID.
+ type: int
+ key:
+ description: MD5 key string.
+ type: str
+ bandwidth:
+ description: Interface bandwidth in kbps (IPv4 only).
+ type: int
+ cost:
+ description: Interface cost metric.
+ type: int
+ dead_interval:
+ description: Dead router detection interval in seconds.
+ type: int
+ hello_interval:
+ description: Hello packet interval in seconds.
+ type: int
+ ifmtu:
+ description: Interface MTU (IPv6 only).
+ type: int
+ instance:
+ description: OSPFv3 instance ID (IPv6 only).
+ type: str
+ mtu_ignore:
+ description: Disable MTU check (IPv4 only).
+ type: bool
+ network:
+ description: Network type (IPv4 only).
+ type: str
+ choices: [broadcast, non-broadcast, point-to-multipoint, point-to-point]
+ passive:
+ description: Disable adjacency formation (IPv6 only).
+ type: bool
+ priority:
+ description: Interface priority.
+ type: int
+ retransmit_interval:
+ description: LSA retransmit interval in seconds.
+ type: int
+ transmit_delay:
+ description: LSA transmit delay in seconds.
+ type: int
+ state:
+ description:
+ - Desired state of the OSPF interface configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces per-interface OSPF config for named interfaces.
+ - C(overridden) replaces all OSPF interface config.
+ - C(deleted) removes OSPF interface config.
+ - 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 OSPF interface configuration
+ vyos.rest.vyos_ospf_interfaces:
+ config:
+ - name: eth1
+ address_family:
+ - afi: ipv4
+ cost: 100
+ transmit_delay: 50
+ priority: 26
+ - afi: ipv6
+ dead_interval: 39
+ passive: true
+ state: merged
+
+- name: Delete OSPF interface configuration
+ vyos.rest.vyos_ospf_interfaces:
+ config:
+ - name: eth1
+ state: deleted
+
+- name: Delete all OSPF interface configuration
+ vyos.rest.vyos_ospf_interfaces:
+ state: deleted
+
+- name: Gather current OSPF interface configuration
+ vyos.rest.vyos_ospf_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: OSPF interface configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: OSPF interface 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 OSPF interface 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
+
+
+_BASE4 = ["protocols", "ospf", "interface"]
+_BASE6 = ["protocols", "ospfv3", "interface"]
+
+# IPv4 scalar fields: argspec_key -> api_key
+_IPV4_SCALARS = {
+ "bandwidth": "bandwidth",
+ "cost": "cost",
+ "dead_interval": "dead-interval",
+ "hello_interval": "hello-interval",
+ "mtu_ignore": "mtu-ignore",
+ "network": "network",
+ "priority": "priority",
+ "retransmit_interval": "retransmit-interval",
+ "transmit_delay": "transmit-delay",
+}
+
+# IPv6 scalar fields
+_IPV6_SCALARS = {
+ "cost": "cost",
+ "dead_interval": "dead-interval",
+ "hello_interval": "hello-interval",
+ "ifmtu": "ifmtu",
+ "instance": "instance-id",
+ "passive": "passive",
+ "priority": "priority",
+ "retransmit_interval": "retransmit-interval",
+ "transmit_delay": "transmit-delay",
+}
+
+# bool fields that use presence (no value)
+_IPV4_BOOL_PRESENCE = {"mtu_ignore"}
+_IPV6_BOOL_PRESENCE = {"passive"}
+
+
+def _parse_ipv4_iface(data):
+ af = {"afi": "ipv4"}
+ data = data or {}
+ for arg_key, api_key in _IPV4_SCALARS.items():
+ if api_key in data:
+ if arg_key in _IPV4_BOOL_PRESENCE:
+ af[arg_key] = True
+ else:
+ val = data[api_key]
+ if arg_key in (
+ "bandwidth",
+ "cost",
+ "dead_interval",
+ "hello_interval",
+ "priority",
+ "retransmit_interval",
+ "transmit_delay",
+ ):
+ try:
+ af[arg_key] = int(val)
+ except (TypeError, ValueError):
+ af[arg_key] = val
+ else:
+ af[arg_key] = val
+ # authentication
+ auth = data.get("authentication", {})
+ if auth:
+ auth_entry = {}
+ if "plaintext-password" in auth:
+ auth_entry["plaintext_password"] = auth["plaintext-password"]
+ md5 = auth.get("md5", {})
+ if md5:
+ key_id_data = md5.get("key-id", {})
+ if key_id_data:
+ key_id = list(key_id_data.keys())[0]
+ md5_key = key_id_data[key_id].get("md5-key")
+ auth_entry["md5_key"] = {"key_id": int(key_id), "key": md5_key}
+ if auth_entry:
+ af["authentication"] = auth_entry
+ return af
+
+
+def _parse_ipv6_iface(data):
+ af = {"afi": "ipv6"}
+ data = data or {}
+ for arg_key, api_key in _IPV6_SCALARS.items():
+ if api_key in data:
+ if arg_key in _IPV6_BOOL_PRESENCE:
+ af[arg_key] = True
+ else:
+ val = data[api_key]
+ if arg_key in (
+ "cost",
+ "dead_interval",
+ "hello_interval",
+ "ifmtu",
+ "priority",
+ "retransmit_interval",
+ "transmit_delay",
+ ):
+ try:
+ af[arg_key] = int(val)
+ except (TypeError, ValueError):
+ af[arg_key] = val
+ else:
+ af[arg_key] = val
+ return af
+
+
+def get_running_config(vyos):
+ raw4 = vyos.get_config(_BASE4) or {}
+ raw4 = raw4.get("interface", raw4)
+ raw6 = vyos.get_config(_BASE6) or {}
+ raw6 = raw6.get("interface", raw6)
+
+ ifaces = {}
+
+ for iface_name, data in raw4.items():
+ if iface_name not in ifaces:
+ ifaces[iface_name] = {"name": iface_name, "address_family": []}
+ af = _parse_ipv4_iface(data)
+ if len(af) > 1: # more than just afi key
+ ifaces[iface_name]["address_family"].append(af)
+
+ for iface_name, data in raw6.items():
+ if iface_name not in ifaces:
+ ifaces[iface_name] = {"name": iface_name, "address_family": []}
+ af = _parse_ipv6_iface(data)
+ if len(af) > 1:
+ ifaces[iface_name]["address_family"].append(af)
+
+ result = sorted(ifaces.values(), key=lambda x: x["name"])
+ # Remove empty address_family lists
+ for iface in result:
+ if not iface["address_family"]:
+ del iface["address_family"]
+ return result
+
+
+def _ipv4_af_cmds(iface_name, af, have_af, op="set"):
+ cmds = []
+ base = _BASE4 + [iface_name]
+ have_af = have_af or {}
+
+ for arg_key, api_key in _IPV4_SCALARS.items():
+ want_val = af.get(arg_key)
+ have_val = have_af.get(arg_key)
+ if want_val is not None and want_val != have_val:
+ if arg_key in _IPV4_BOOL_PRESENCE:
+ cmds.append(("set", base + [api_key]))
+ else:
+ cmds.append(("set", base + [api_key, str(want_val)]))
+ elif op == "replace" and have_val is not None and want_val != have_val:
+ cmds.append(("delete", base + [api_key]))
+
+ # authentication
+ want_auth = af.get("authentication") or {}
+ have_auth = have_af.get("authentication") or {}
+ if want_auth.get("plaintext_password") and want_auth["plaintext_password"] != have_auth.get(
+ "plaintext_password",
+ ):
+ cmds.append(
+ (
+ "set",
+ base
+ + [
+ "authentication",
+ "plaintext-password",
+ want_auth["plaintext_password"],
+ ],
+ ),
+ )
+ md5 = want_auth.get("md5_key") or {}
+ if md5 and md5 != have_auth.get("md5_key"):
+ cmds.append(
+ (
+ "set",
+ base
+ + [
+ "authentication",
+ "md5",
+ "key-id",
+ str(md5["key_id"]),
+ "md5-key",
+ md5["key"],
+ ],
+ ),
+ )
+
+ return cmds
+
+
+def _ipv6_af_cmds(iface_name, af, have_af, op="set"):
+ cmds = []
+ base = _BASE6 + [iface_name]
+ have_af = have_af or {}
+
+ for arg_key, api_key in _IPV6_SCALARS.items():
+ want_val = af.get(arg_key)
+ have_val = have_af.get(arg_key)
+ if want_val is not None and want_val != have_val:
+ if arg_key in _IPV6_BOOL_PRESENCE:
+ cmds.append(("set", base + [api_key]))
+ else:
+ cmds.append(("set", base + [api_key, str(want_val)]))
+ elif op == "replace" and have_val is not None and want_val != have_val:
+ cmds.append(("delete", base + [api_key]))
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+ have_map = {e["name"]: e for e in have_raw}
+
+ if state == "deleted":
+ if not config:
+ # delete all
+ if have_raw:
+ for iface in have_raw:
+ name = iface["name"]
+ afs = {af["afi"] for af in iface.get("address_family", [])}
+ if "ipv4" in afs:
+ cmds.append(("delete", _BASE4 + [name]))
+ if "ipv6" in afs:
+ cmds.append(("delete", _BASE6 + [name]))
+ else:
+ for entry in config:
+ name = entry["name"]
+ if name in have_map:
+ have_afs = {af["afi"] for af in have_map[name].get("address_family", [])}
+ want_afis = {af["afi"] for af in (entry.get("address_family") or [])}
+ if not want_afis:
+ # delete all AFIs for this interface
+ if "ipv4" in have_afs:
+ cmds.append(("delete", _BASE4 + [name]))
+ if "ipv6" in have_afs:
+ cmds.append(("delete", _BASE6 + [name]))
+ else:
+ if "ipv4" in want_afis and "ipv4" in have_afs:
+ cmds.append(("delete", _BASE4 + [name]))
+ if "ipv6" in want_afis and "ipv6" in have_afs:
+ cmds.append(("delete", _BASE6 + [name]))
+ return cmds
+
+ if state == "overridden":
+ want_names = {e["name"] for e in config}
+ for name, have_iface in have_map.items():
+ if name not in want_names:
+ have_afs = {af["afi"] for af in have_iface.get("address_family", [])}
+ if "ipv4" in have_afs:
+ cmds.append(("delete", _BASE4 + [name]))
+ if "ipv6" in have_afs:
+ cmds.append(("delete", _BASE6 + [name]))
+
+ for entry in config:
+ name = entry["name"]
+ have_iface = have_map.get(name, {})
+ have_af_map = {af["afi"]: af for af in have_iface.get("address_family", [])}
+
+ for af in entry.get("address_family", []):
+ afi = af["afi"]
+ have_af = have_af_map.get(afi, {})
+
+ if state == "replaced":
+ af_clean = {k: v for k, v in af.items() if v is not None}
+ if have_af and have_af != af_clean:
+ if afi == "ipv4":
+ cmds.append(("delete", _BASE4 + [name]))
+ have_af = {}
+ else:
+ cmds.append(("delete", _BASE6 + [name]))
+ have_af = {}
+ elif have_af == af_clean:
+ continue # already matches — idempotent
+
+ if afi == "ipv4":
+ cmds += _ipv4_af_cmds(name, af, have_af)
+ else:
+ cmds += _ipv6_af_cmds(name, af, have_af)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ address_family=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ afi=dict(type="str", choices=["ipv4", "ipv6"], required=True),
+ authentication=dict(
+ type="dict",
+ options=dict(
+ plaintext_password=dict(type="str", no_log=True),
+ md5_key=dict(
+ type="dict",
+ no_log=True,
+ options=dict(
+ key_id=dict(type="int"),
+ key=dict(type="str", no_log=True),
+ ),
+ ),
+ ),
+ ),
+ bandwidth=dict(type="int"),
+ cost=dict(type="int"),
+ dead_interval=dict(type="int"),
+ hello_interval=dict(type="int"),
+ ifmtu=dict(type="int"),
+ instance=dict(type="str"),
+ mtu_ignore=dict(type="bool"),
+ network=dict(
+ type="str",
+ choices=[
+ "broadcast",
+ "non-broadcast",
+ "point-to-multipoint",
+ "point-to-point",
+ ],
+ ),
+ passive=dict(type="bool"),
+ priority=dict(type="int"),
+ retransmit_interval=dict(type="int"),
+ transmit_delay=dict(type="int"),
+ ),
+ ),
+ ),
+ ),
+ 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()