summaryrefslogtreecommitdiff
path: root/plugins
diff options
context:
space:
mode:
authoromnom62 <75066712+omnom62@users.noreply.github.com>2026-08-22 01:04:18 +1000
committerGitHub <noreply@github.com>2026-08-21 10:04:18 -0500
commitcb738721c15ac01f49f66144dae80eb478331f77 (patch)
tree52f870ed414506aaa5cd6efa7ae6513d807e194b /plugins
parent885b9462480712210ddeea9ca4a4b7a52e9ef587 (diff)
downloadrest.vyos-cb738721c15ac01f49f66144dae80eb478331f77.tar.gz
rest.vyos-cb738721c15ac01f49f66144dae80eb478331f77.zip
T8989: wave 2 modules: interfaces, l3_interfaces, lag_interfaces, lldp_interfaces, ospfv2/3, ospf_interfaces
* T8989: vyos_interfaces * T8989: vyos_interfaces changelog * T8989: vyos_interfaces changelog * T8989: changelog typo * T8989: vyos_l3_interfaces module * T8989: vyos_l3_interfaces module SIT and UAT * T8989: vyos_l3_interfaces doc * T8989: lag_interfaces * T8989: lag interfaces * T8989: lldp_interfaces * T8989: lldp_interfaces module * T8989: SIT updated * T8989: SIT updated * T8989: SIT updated * T8989: typo fixes * T8989: SIT and UAT updates * T8989: ospf_v3 module * T8989: ospf_v3 module * T8989: vyos_ospf_interfaces * T8989: vyos_ospf_interfaces SIT * T8989: vyos_ospf_interfaces SIT * T8989: vyos_ospfv3 SIT and UAT --------- Co-authored-by: John Estabrook <jestabro@vyos.io>
Diffstat (limited to 'plugins')
-rw-r--r--plugins/modules/vyos_banner.py2
-rw-r--r--plugins/modules/vyos_interfaces.py334
-rw-r--r--plugins/modules/vyos_l3_interfaces.py467
-rw-r--r--plugins/modules/vyos_lag_interfaces.py431
-rw-r--r--plugins/modules/vyos_lldp_interfaces.py365
-rw-r--r--plugins/modules/vyos_logging_global.py8
-rw-r--r--plugins/modules/vyos_ospf_interfaces.py539
-rw-r--r--plugins/modules/vyos_ospfv2.py955
-rw-r--r--plugins/modules/vyos_ospfv3.py360
-rw-r--r--plugins/modules/vyos_route_maps.py15
-rw-r--r--plugins/modules/vyos_snmp_server.py10
11 files changed, 3471 insertions, 15 deletions
diff --git a/plugins/modules/vyos_banner.py b/plugins/modules/vyos_banner.py
index 304fa20..bcda26d 100644
--- a/plugins/modules/vyos_banner.py
+++ b/plugins/modules/vyos_banner.py
@@ -267,7 +267,7 @@ def main():
try:
if state in ("merged", "replaced"):
- if state == "merged" and before.get("text") == desired_text:
+ if before.get("text") == desired_text:
module.exit_json(changed=False, before=before, commands=[])
client.configure_set(path, _encode_banner(desired_text))
commands.append("set {p} '...'".format(p=" ".join(path)))
diff --git a/plugins/modules/vyos_interfaces.py b/plugins/modules/vyos_interfaces.py
new file mode 100644
index 0000000..6ed45d5
--- /dev/null
+++ b/plugins/modules/vyos_interfaces.py
@@ -0,0 +1,334 @@
+#!/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_interfaces
+short_description: Manage interface configuration on VyOS devices via REST API.
+description:
+ - Manages L2 interface configuration (description, MTU, speed, duplex, enabled)
+ on VyOS devices using the HTTPS REST API.
+ - IP address configuration is handled by M(vyos.rest.vyos_l3_interfaces).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Full interface name (e.g. eth0, bond0, lo).
+ type: str
+ required: true
+ description:
+ description: Interface description.
+ type: str
+ enabled:
+ description: Whether the interface is enabled. False sets the disable flag.
+ type: bool
+ default: true
+ mtu:
+ description: Interface MTU.
+ type: int
+ duplex:
+ description: Interface duplex setting.
+ type: str
+ choices: [auto, full, half]
+ speed:
+ description: Interface speed setting.
+ type: str
+ choices: [auto, "10", "100", "1000", "2500", "10000"]
+ state:
+ description:
+ - C(merged) - Merge config with existing interface settings.
+ - C(replaced) - Replace config for listed interfaces.
+ - C(overridden) - Replace config for all interfaces.
+ - C(deleted) - Remove listed interface config or all interface config.
+ - C(gathered) - Read interface config from device without changes.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered]
+ default: merged
+seealso:
+ - module: vyos.vyos.vyos_interfaces
+ - module: vyos.rest.vyos_l3_interfaces
+"""
+
+EXAMPLES = r"""
+- name: Merge interface configuration
+ vyos.rest.vyos_interfaces:
+ config:
+ - name: eth0
+ description: Management interface
+ mtu: 1500
+ enabled: true
+ state: merged
+
+- name: Disable an interface
+ vyos.rest.vyos_interfaces:
+ config:
+ - name: eth1
+ enabled: false
+ state: merged
+
+- name: Delete interface description
+ vyos.rest.vyos_interfaces:
+ config:
+ - name: eth0
+ state: deleted
+
+- name: Gather current interface configuration
+ vyos.rest.vyos_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: Interface configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: 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 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
+
+
+# Interface name prefix → API type key
+_IFACE_TYPE = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "lo": "loopback",
+ "tun": "tunnel",
+ "wg": "wireguard",
+ "vti": "vti",
+ "dum": "dummy",
+ "vtun": "openvpn",
+ "ppp": "pppoe",
+ "wlan": "wireless",
+ "br": "bridge",
+}
+
+# L2 fields managed by this module — excludes address, hw-id etc.
+_L2_FIELDS = ["description", "mtu", "duplex", "speed"]
+
+
+def _iface_type(name):
+ for prefix, itype in _IFACE_TYPE.items():
+ if name.startswith(prefix):
+ return itype
+ return "ethernet"
+
+
+def _iface_base(name):
+ return ["interfaces", _iface_type(name), name]
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(["interfaces"])
+ if not raw or not isinstance(raw, dict):
+ return []
+
+ result = []
+ for itype, ifaces in sorted(raw.items()):
+ if not isinstance(ifaces, dict):
+ continue
+ for iname, idata in sorted(ifaces.items()):
+ idata = idata or {}
+ entry = {"name": iname}
+ if idata.get("description"):
+ entry["description"] = idata["description"]
+ if "mtu" in idata:
+ entry["mtu"] = int(idata["mtu"])
+ if "duplex" in idata:
+ entry["duplex"] = idata["duplex"]
+ if "speed" in idata:
+ entry["speed"] = idata["speed"]
+ entry["enabled"] = "disable" not in idata
+ result.append(entry)
+
+ return result
+
+
+def _normalize(config):
+ """Convert argspec list to dict keyed by interface name."""
+ return {entry["name"]: entry for entry in (config or [])}
+
+
+def _iface_cmds(name, want, have):
+ """Generate set/delete commands to bring have → want for one interface."""
+ cmds = []
+ base = _iface_base(name)
+ have = have or {}
+
+ # description
+ want_desc = want.get("description")
+ have_desc = have.get("description")
+ if want_desc is not None and want_desc != have_desc:
+ cmds.append(("set", base + ["description", want_desc]))
+ elif want_desc is None and have_desc is not None:
+ cmds.append(("delete", base + ["description"]))
+
+ # mtu
+ want_mtu = want.get("mtu")
+ have_mtu = have.get("mtu")
+ if want_mtu is not None and want_mtu != have_mtu:
+ cmds.append(("set", base + ["mtu", str(want_mtu)]))
+
+ # duplex
+ want_duplex = want.get("duplex")
+ have_duplex = have.get("duplex")
+ if want_duplex is not None and want_duplex != have_duplex:
+ cmds.append(("set", base + ["duplex", want_duplex]))
+
+ # speed
+ want_speed = want.get("speed")
+ have_speed = have.get("speed")
+ if want_speed is not None and want_speed != have_speed:
+ cmds.append(("set", base + ["speed", want_speed]))
+
+ # enabled / disable flag
+ want_enabled = want.get("enabled", True)
+ have_enabled = have.get("enabled", True)
+ if not want_enabled and have_enabled:
+ cmds.append(("set", base + ["disable"]))
+ elif want_enabled and not have_enabled:
+ cmds.append(("delete", base + ["disable"]))
+
+ return cmds
+
+
+def _delete_iface_config(name, have):
+ """Generate delete commands to remove L2 config from an interface."""
+ cmds = []
+ base = _iface_base(name)
+ have = have or {}
+
+ for field in _L2_FIELDS:
+ if field in have:
+ cmds.append(("delete", base + [field]))
+ if not have.get("enabled", True):
+ cmds.append(("delete", base + ["disable"]))
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+ have_map = _normalize(have_raw)
+
+ if state == "deleted":
+ if not config:
+ for name, have in have_map.items():
+ cmds += _delete_iface_config(name, have)
+ else:
+ for entry in config:
+ name = entry["name"]
+ cmds += _delete_iface_config(name, have_map.get(name, {}))
+ return cmds
+
+ want_map = _normalize(config)
+
+ if state == "overridden":
+ # delete L2 config from interfaces not in want
+ for name in set(have_map) - set(want_map):
+ cmds += _delete_iface_config(name, have_map[name])
+
+ for name, want in want_map.items():
+ have = have_map.get(name, {})
+
+ if state == "replaced":
+ # pre-check — only act if something differs
+ test_cmds = _iface_cmds(name, want, have)
+ if not test_cmds:
+ continue
+ # delete L2 fields then rebuild
+ cmds += _delete_iface_config(name, have)
+ have = {}
+
+ cmds += _iface_cmds(name, want, have if state != "replaced" else {})
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ description=dict(type="str"),
+ enabled=dict(type="bool", default=True),
+ mtu=dict(type="int"),
+ duplex=dict(type="str", choices=["auto", "full", "half"]),
+ speed=dict(type="str", choices=["auto", "10", "100", "1000", "2500", "10000"]),
+ ),
+ ),
+ state=dict(
+ type="str",
+ 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/plugins/modules/vyos_l3_interfaces.py b/plugins/modules/vyos_l3_interfaces.py
new file mode 100644
index 0000000..9b33973
--- /dev/null
+++ b/plugins/modules/vyos_l3_interfaces.py
@@ -0,0 +1,467 @@
+#!/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_l3_interfaces
+short_description: Manage L3 interface configuration on VyOS devices via REST API.
+description:
+ - Manages IPv4 and IPv6 address configuration on VyOS interfaces using the
+ HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_l3_interfaces) but uses the HTTP API instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of L3 interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Full name of the interface, e.g. eth0, lo.
+ type: str
+ required: true
+ ipv4:
+ description: List of IPv4 addresses of the interface.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description:
+ - IPv4 address in CIDR notation or C(dhcp).
+ type: str
+ ipv6:
+ description: List of IPv6 addresses of the interface.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description:
+ - IPv6 address in CIDR notation, C(dhcpv6), or C(auto-config).
+ type: str
+ vifs:
+ description: List of virtual sub-interfaces (VLANs).
+ type: list
+ elements: dict
+ suboptions:
+ vlan_id:
+ description: VLAN identifier.
+ type: int
+ required: true
+ ipv4:
+ description: List of IPv4 addresses of the VIF.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: IPv4 address in CIDR notation or C(dhcp).
+ type: str
+ ipv6:
+ description: List of IPv6 addresses of the VIF.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: IPv6 address in CIDR notation, C(dhcpv6), or C(auto-config).
+ type: str
+ running_config:
+ description: Used only with state C(parsed).
+ type: str
+ state:
+ description:
+ - C(merged) - Add addresses without removing existing ones.
+ - C(replaced) - Replace addresses for listed interfaces.
+ - C(overridden) - Replace addresses for all interfaces.
+ - C(deleted) - Remove listed or all interface addresses.
+ - C(gathered) - Read interface addresses from device without changes.
+ - C(rendered) - Return commands for provided config without connecting.
+ - C(parsed) - Parse running_config into structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed]
+ default: merged
+seealso:
+ - module: vyos.vyos.vyos_l3_interfaces
+ - module: vyos.rest.vyos_interfaces
+"""
+
+EXAMPLES = r"""
+- name: Merge L3 interface configuration
+ vyos.rest.vyos_l3_interfaces:
+ config:
+ - name: eth0
+ ipv4:
+ - address: 192.0.2.1/24
+ - address: dhcp
+ - name: lo
+ ipv4:
+ - address: 10.0.0.1/32
+ ipv6:
+ - address: 2001:db8::1/128
+ state: merged
+
+- name: Add VLAN subinterface addresses
+ vyos.rest.vyos_l3_interfaces:
+ config:
+ - name: eth0
+ vifs:
+ - vlan_id: 100
+ ipv4:
+ - address: 192.0.2.100/24
+ state: merged
+
+- name: Delete all interface addresses
+ vyos.rest.vyos_l3_interfaces:
+ state: deleted
+
+- name: Gather current L3 interface configuration
+ vyos.rest.vyos_l3_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: L3 interface configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: L3 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 L3 interface configuration as structured data.
+ returned: when state is gathered
+ type: list
+rendered:
+ description: Commands for the provided config (state=rendered).
+ returned: when state is rendered
+ type: list
+parsed:
+ description: Structured data parsed from running_config (state=parsed).
+ returned: when state is parsed
+ 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
+
+
+_IFACE_TYPE = {
+ "eth": "ethernet",
+ "bond": "bonding",
+ "lo": "loopback",
+ "tun": "tunnel",
+ "wg": "wireguard",
+ "vti": "vti",
+ "dum": "dummy",
+ "vtun": "openvpn",
+ "br": "bridge",
+}
+
+
+def _iface_type(name):
+ for prefix, itype in _IFACE_TYPE.items():
+ if name.startswith(prefix):
+ return itype
+ return "ethernet"
+
+
+def _iface_base(name):
+ return ["interfaces", _iface_type(name), name]
+
+
+def _addr_list(raw):
+ """Normalize address field — string or list → sorted list."""
+ if not raw:
+ return []
+ if isinstance(raw, str):
+ return [raw]
+ if isinstance(raw, list):
+ return sorted(raw)
+ return []
+
+
+def _split_addresses(addresses):
+ """Split address list into ipv4 and ipv6 lists."""
+ ipv4 = []
+ ipv6 = []
+ for addr in addresses:
+ if addr in ("dhcp", "dhcpv6"):
+ if addr == "dhcp":
+ ipv4.append(addr)
+ else:
+ ipv6.append(addr)
+ elif ":" in addr:
+ ipv6.append(addr)
+ else:
+ ipv4.append(addr)
+ return sorted(ipv4), sorted(ipv6)
+
+
+def _parse_iface(name, idata):
+ """Parse raw API interface data into argspec format."""
+ idata = idata or {}
+ entry = {"name": name}
+
+ addrs = _addr_list(idata.get("address"))
+ if addrs:
+ ipv4, ipv6 = _split_addresses(addrs)
+ if ipv4:
+ entry["ipv4"] = [{"address": a} for a in ipv4]
+ if ipv6:
+ entry["ipv6"] = [{"address": a} for a in ipv6]
+
+ vif_data = idata.get("vif") or {}
+ if isinstance(vif_data, dict) and vif_data:
+ vifs = []
+ for vlan_id, vdata in sorted(vif_data.items(), key=lambda x: int(x[0])):
+ vdata = vdata or {}
+ vif = {"vlan_id": int(vlan_id)}
+ vaddrs = _addr_list(vdata.get("address"))
+ if vaddrs:
+ vipv4, vipv6 = _split_addresses(vaddrs)
+ if vipv4:
+ vif["ipv4"] = [{"address": a} for a in vipv4]
+ if vipv6:
+ vif["ipv6"] = [{"address": a} for a in vipv6]
+ vifs.append(vif)
+ if vifs:
+ entry["vifs"] = vifs
+
+ return entry
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(["interfaces"])
+ if not raw or not isinstance(raw, dict):
+ return []
+
+ result = []
+ for itype, ifaces in sorted(raw.items()):
+ if not isinstance(ifaces, dict):
+ continue
+ for iname, idata in sorted(ifaces.items()):
+ entry = _parse_iface(iname, idata)
+ # only include if there's at least one address or vif
+ if entry.get("ipv4") or entry.get("ipv6") or entry.get("vifs"):
+ result.append(entry)
+
+ return result
+
+
+def _normalize(config):
+ """Convert argspec list to dict keyed by interface name."""
+ result = {}
+ for entry in config or []:
+ name = entry["name"]
+ ipv4 = sorted([a["address"] for a in (entry.get("ipv4") or [])])
+ ipv6 = sorted([a["address"] for a in (entry.get("ipv6") or [])])
+ vifs = {}
+ for vif in entry.get("vifs") or []:
+ vid = vif["vlan_id"]
+ vipv4 = sorted([a["address"] for a in (vif.get("ipv4") or [])])
+ vipv6 = sorted([a["address"] for a in (vif.get("ipv6") or [])])
+ vifs[vid] = {"ipv4": vipv4, "ipv6": vipv6}
+ result[name] = {"ipv4": ipv4, "ipv6": ipv6, "vifs": vifs}
+ return result
+
+
+def _addr_cmds(base, want_addrs, have_addrs, state):
+ """Generate set/delete commands for address lists."""
+ cmds = []
+ want_set = set(want_addrs)
+ have_set = set(have_addrs)
+
+ for addr in want_set - have_set:
+ cmds.append(("set", base + ["address", addr]))
+
+ if state in ("replaced", "deleted", "overridden"):
+ for addr in have_set - want_set:
+ cmds.append(("delete", base + ["address", addr]))
+
+ return cmds
+
+
+def _vif_cmds(iface_base, want_vifs, have_vifs, state):
+ """Generate commands for VIF subinterfaces."""
+ cmds = []
+
+ if state in ("replaced", "overridden"):
+ for vid in set(have_vifs) - set(want_vifs):
+ cmds.append(("delete", iface_base + ["vif", str(vid)]))
+
+ for vid, want_vif in want_vifs.items():
+ have_vif = have_vifs.get(vid, {"ipv4": [], "ipv6": []})
+ vif_base = iface_base + ["vif", str(vid)]
+ cmds += _addr_cmds(vif_base, want_vif["ipv4"], have_vif["ipv4"], state)
+ cmds += _addr_cmds(vif_base, want_vif["ipv6"], have_vif["ipv6"], state)
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+ have_map = _normalize(have_raw)
+
+ if state == "deleted":
+ if not config:
+ for name, have in have_map.items():
+ base = _iface_base(name)
+ for addr in have["ipv4"] + have["ipv6"]:
+ cmds.append(("delete", base + ["address", addr]))
+ for vid in have["vifs"]:
+ cmds.append(("delete", base + ["vif", str(vid)]))
+ else:
+ want_map = _normalize(config)
+ for name, want in want_map.items():
+ have = have_map.get(name, {"ipv4": [], "ipv6": [], "vifs": {}})
+ base = _iface_base(name)
+ if not want["ipv4"] and not want["ipv6"] and not want["vifs"]:
+ # delete all addresses for this interface
+ for addr in have["ipv4"] + have["ipv6"]:
+ cmds.append(("delete", base + ["address", addr]))
+ for vid in have["vifs"]:
+ cmds.append(("delete", base + ["vif", str(vid)]))
+ else:
+ for addr in want["ipv4"] + want["ipv6"]:
+ if addr in have["ipv4"] + have["ipv6"]:
+ cmds.append(("delete", base + ["address", addr]))
+ for vid in want["vifs"]:
+ if vid in have["vifs"]:
+ cmds.append(("delete", base + ["vif", str(vid)]))
+ return cmds
+
+ want_map = _normalize(config)
+
+ if state == "overridden":
+ for name in set(have_map) - set(want_map):
+ have = have_map[name]
+ base = _iface_base(name)
+ for addr in have["ipv4"] + have["ipv6"]:
+ cmds.append(("delete", base + ["address", addr]))
+ for vid in have["vifs"]:
+ cmds.append(("delete", base + ["vif", str(vid)]))
+
+ for name, want in want_map.items():
+ have = have_map.get(name, {"ipv4": [], "ipv6": [], "vifs": {}})
+ base = _iface_base(name)
+ cmds += _addr_cmds(base, want["ipv4"], have["ipv4"], state)
+ cmds += _addr_cmds(base, want["ipv6"], have["ipv6"], state)
+ cmds += _vif_cmds(base, want["vifs"], have["vifs"], state)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ ipv4=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ ipv6=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ vifs=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ vlan_id=dict(type="int", required=True),
+ ipv4=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ ipv6=dict(
+ type="list",
+ elements="dict",
+ options=dict(address=dict(type="str")),
+ ),
+ ),
+ ),
+ ),
+ ),
+ running_config=dict(type="str"),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=ARGUMENT_SPEC,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ("state", "rendered", ["config"]),
+ ("state", "parsed", ["running_config"]),
+ ],
+ supports_check_mode=True,
+ )
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ if state == "parsed":
+ # parsed is offline — just return empty for now
+ module.exit_json(parsed=[])
+
+ if state == "rendered":
+ # build commands without connecting
+ cmds = build_commands(config, [], "merged")
+ module.exit_json(rendered=cmds, commands=cmds)
+
+ 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/plugins/modules/vyos_lag_interfaces.py b/plugins/modules/vyos_lag_interfaces.py
new file mode 100644
index 0000000..6855419
--- /dev/null
+++ b/plugins/modules/vyos_lag_interfaces.py
@@ -0,0 +1,431 @@
+#!/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_lag_interfaces
+short_description: Manage LAG interface configuration on VyOS devices via REST API.
+description:
+ - Manages Link Aggregation Group (LAG/bonding) interface configuration on VyOS
+ devices using the HTTPS REST API.
+ - Mirrors C(vyos.vyos.vyos_lag_interfaces) but uses the HTTP API instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of LAG interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Name of the LAG interface (e.g. bond0).
+ type: str
+ required: true
+ mode:
+ description: LAG bonding mode.
+ type: str
+ choices:
+ - 802.3ad
+ - active-backup
+ - broadcast
+ - round-robin
+ - transmit-load-balance
+ - adaptive-load-balance
+ - xor-hash
+ members:
+ description: List of member interfaces.
+ type: list
+ elements: dict
+ suboptions:
+ member:
+ description: Name of the member interface.
+ type: str
+ primary:
+ description: Primary interface for active-backup mode.
+ type: str
+ hash_policy:
+ description: Transmit hash policy.
+ type: str
+ choices:
+ - layer2
+ - layer2+3
+ - layer3+4
+ arp_monitor:
+ description: ARP link monitoring parameters.
+ type: dict
+ suboptions:
+ interval:
+ description: ARP monitoring interval in milliseconds.
+ type: int
+ target:
+ description: IP addresses to use for ARP monitoring.
+ type: list
+ elements: str
+ running_config:
+ description: Used only with state C(parsed).
+ type: str
+ state:
+ description:
+ - C(merged) - Merge config with existing LAG settings.
+ - C(replaced) - Replace config for listed LAG interfaces.
+ - C(overridden) - Replace config for all LAG interfaces.
+ - C(deleted) - Remove listed or all LAG interface config.
+ - C(gathered) - Read LAG config from device without changes.
+ - C(rendered) - Return commands for provided config without connecting.
+ - C(parsed) - Parse running_config into structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed]
+ default: merged
+seealso:
+ - module: vyos.vyos.vyos_lag_interfaces
+"""
+
+EXAMPLES = r"""
+- name: Merge LAG interface configuration
+ vyos.rest.vyos_lag_interfaces:
+ config:
+ - name: bond0
+ mode: 802.3ad
+ hash_policy: layer2
+ members:
+ - member: eth1
+ - member: eth2
+ state: merged
+
+- name: Delete all LAG interfaces
+ vyos.rest.vyos_lag_interfaces:
+ state: deleted
+
+- name: Gather current LAG configuration
+ vyos.rest.vyos_lag_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: LAG configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: LAG 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 LAG configuration as structured data.
+ returned: when state is gathered
+ type: list
+rendered:
+ description: Commands for provided config (state=rendered).
+ returned: when state is rendered
+ type: list
+parsed:
+ description: Structured data parsed from running_config (state=parsed).
+ returned: when state is parsed
+ 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
+
+
+_BASE = ["interfaces", "bonding"]
+
+
+def _bond_base(name):
+ return _BASE + [name]
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return []
+
+ if len(raw) == 1 and "bonding" in raw:
+ raw = raw["bonding"]
+
+ result = []
+ for name, data in sorted(raw.items()):
+ if name in ("bonding", "ethernet", "loopback"):
+ continue
+ data = data or {}
+ entry = {"name": name}
+
+ if data.get("mode"):
+ entry["mode"] = data["mode"]
+ if data.get("primary"):
+ entry["primary"] = data["primary"]
+ if "hash-policy" in data:
+ entry["hash_policy"] = data["hash-policy"]
+
+ member_data = data.get("member", {})
+ if isinstance(member_data, dict):
+ iface_data = member_data.get("interface", {})
+ if isinstance(iface_data, dict) and iface_data:
+ entry["members"] = [{"member": m} for m in sorted(iface_data.keys())]
+ elif isinstance(iface_data, str):
+ entry["members"] = [{"member": iface_data}]
+
+ arp = data.get("arp-monitor", {})
+ if isinstance(arp, dict) and arp:
+ arp_entry = {}
+ if "interval" in arp:
+ arp_entry["interval"] = int(arp["interval"])
+ target_data = arp.get("target", {})
+ if isinstance(target_data, dict):
+ arp_entry["target"] = sorted(target_data.keys())
+ elif isinstance(target_data, str):
+ arp_entry["target"] = [target_data]
+ if arp_entry:
+ entry["arp_monitor"] = arp_entry
+
+ result.append(entry)
+
+ return result
+
+
+def _normalize(config):
+ result = {}
+ for entry in config or []:
+ name = entry["name"]
+ result[name] = {
+ "mode": entry.get("mode"),
+ "primary": entry.get("primary"),
+ "hash_policy": entry.get("hash_policy"),
+ "members": sorted([m["member"] for m in (entry.get("members") or [])]),
+ "arp_interval": (entry.get("arp_monitor") or {}).get("interval"),
+ "arp_targets": sorted((entry.get("arp_monitor") or {}).get("target") or []),
+ }
+ return result
+
+
+def _bond_cmds(name, want, have):
+ cmds = []
+ base = _bond_base(name)
+ have = have or {}
+
+ if want.get("mode") and want["mode"] != have.get("mode"):
+ cmds.append(("set", base + ["mode", want["mode"]]))
+
+ if want.get("primary") and want["primary"] != have.get("primary"):
+ cmds.append(("set", base + ["primary", want["primary"]]))
+
+ if want.get("hash_policy") and want["hash_policy"] != have.get("hash_policy"):
+ cmds.append(("set", base + ["hash-policy", want["hash_policy"]]))
+
+ want_members = set(want.get("members") or [])
+ have_members = set(have.get("members") or [])
+ for m in want_members - have_members:
+ cmds.append(("set", base + ["member", "interface", m]))
+
+ want_interval = want.get("arp_interval")
+ have_interval = have.get("arp_interval")
+ if want_interval is not None and want_interval != have_interval:
+ cmds.append(("set", base + ["arp-monitor", "interval", str(want_interval)]))
+
+ want_targets = set(want.get("arp_targets") or [])
+ have_targets = set(have.get("arp_targets") or [])
+ for t in want_targets - have_targets:
+ cmds.append(("set", base + ["arp-monitor", "target", t]))
+
+ return cmds
+
+
+def _delete_bond_cmds(name, have, want=None):
+ """Generate delete commands for a bond — full delete or selective."""
+ cmds = []
+ base = _bond_base(name)
+ have = have or {}
+ want = want or {}
+
+ if not want:
+ # full delete
+ cmds.append(("delete", base))
+ return cmds
+
+ # selective — only delete what want specifies
+ if want.get("mode") and have.get("mode"):
+ cmds.append(("delete", base + ["mode"]))
+ if want.get("primary") and have.get("primary"):
+ cmds.append(("delete", base + ["primary"]))
+ if want.get("hash_policy") and have.get("hash_policy"):
+ cmds.append(("delete", base + ["hash-policy"]))
+ for m in set(want.get("members") or []) & set(have.get("members") or []):
+ cmds.append(("delete", base + ["member", "interface", m]))
+ if want.get("arp_interval") and have.get("arp_interval"):
+ cmds.append(("delete", base + ["arp-monitor", "interval"]))
+ for t in set(want.get("arp_targets") or []) & set(have.get("arp_targets") or []):
+ cmds.append(("delete", base + ["arp-monitor", "target", t]))
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+ have_map = _normalize(have_raw)
+
+ if state == "deleted":
+ if not config:
+ for name in have_map:
+ cmds.append(("delete", _bond_base(name)))
+ else:
+ want_map = _normalize(config)
+ for name, want in want_map.items():
+ have = have_map.get(name, {})
+ if not any(
+ [
+ want.get("mode"),
+ want.get("primary"),
+ want.get("hash_policy"),
+ want.get("members"),
+ want.get("arp_interval"),
+ want.get("arp_targets"),
+ ],
+ ):
+ # delete entire bond
+ if name in have_map:
+ cmds.append(("delete", _bond_base(name)))
+ else:
+ cmds += _delete_bond_cmds(name, have, want)
+ return cmds
+
+ want_map = _normalize(config)
+
+ if state == "overridden":
+ for name in set(have_map) - set(want_map):
+ cmds.append(("delete", _bond_base(name)))
+
+ for name, want in want_map.items():
+ have = have_map.get(name, {})
+
+ if state == "replaced" and name in have_map:
+ test_cmds = _bond_cmds(name, want, have)
+ # check for extra members/targets in have not in want
+ extra_members = set(have.get("members") or []) - set(want.get("members") or [])
+ extra_targets = set(have.get("arp_targets") or []) - set(want.get("arp_targets") or [])
+ have_fields = {k: v for k, v in have.items() if v}
+ want_fields = {k: v for k, v in want.items() if v}
+ if test_cmds or extra_members or extra_targets or have_fields != want_fields:
+ cmds.append(("delete", _bond_base(name)))
+ have = {}
+ else:
+ continue
+
+ cmds += _bond_cmds(name, want, have)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ mode=dict(
+ type="str",
+ choices=[
+ "802.3ad",
+ "active-backup",
+ "broadcast",
+ "round-robin",
+ "transmit-load-balance",
+ "adaptive-load-balance",
+ "xor-hash",
+ ],
+ ),
+ members=dict(
+ type="list",
+ elements="dict",
+ options=dict(member=dict(type="str")),
+ ),
+ primary=dict(type="str"),
+ hash_policy=dict(
+ type="str",
+ choices=["layer2", "layer2+3", "layer3+4"],
+ ),
+ arp_monitor=dict(
+ type="dict",
+ options=dict(
+ interval=dict(type="int"),
+ target=dict(type="list", elements="str"),
+ ),
+ ),
+ ),
+ ),
+ running_config=dict(type="str"),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=ARGUMENT_SPEC,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ("state", "rendered", ["config"]),
+ ("state", "parsed", ["running_config"]),
+ ],
+ supports_check_mode=True,
+ )
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ if state == "parsed":
+ module.exit_json(parsed=[])
+
+ if state == "rendered":
+ cmds = build_commands(config, [], "merged")
+ module.exit_json(rendered=cmds, commands=cmds)
+
+ 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/plugins/modules/vyos_lldp_interfaces.py b/plugins/modules/vyos_lldp_interfaces.py
new file mode 100644
index 0000000..aa80b2f
--- /dev/null
+++ b/plugins/modules/vyos_lldp_interfaces.py
@@ -0,0 +1,365 @@
+#!/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_lldp_interfaces
+short_description: Manage LLDP interface configuration on VyOS devices via REST API.
+description:
+ - Manages per-interface LLDP configuration on VyOS devices using the HTTPS REST API.
+ - Targets VyOS 1.5+ where LLDP interface mode replaces the legacy disable flag.
+ - For the disable flag used in VyOS 1.3/1.4, see C(vyos.vyos.vyos_lldp_interfaces).
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: List of LLDP interface configurations.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description: Name of the interface.
+ type: str
+ required: true
+ mode:
+ description:
+ - LLDP administrative mode for this interface.
+ - C(rx-tx) sends and receives LLDP frames (default).
+ - C(disable) disables LLDP on this interface.
+ - C(rx) receives only.
+ - C(tx) transmits only.
+ type: str
+ choices: [disable, rx-tx, rx, tx]
+ location:
+ description: LLDP-MED location data.
+ type: dict
+ suboptions:
+ elin:
+ description: Emergency Call Service ELIN number (10-25 digits).
+ type: str
+ coordinate_based:
+ description: Coordinate-based location.
+ type: dict
+ suboptions:
+ latitude:
+ description: Latitude (e.g. 33.524449N).
+ type: str
+ required: true
+ longitude:
+ description: Longitude (e.g. 22.267255E).
+ type: str
+ required: true
+ altitude:
+ description: Altitude in meters.
+ type: int
+ datum:
+ description: Coordinate datum type.
+ type: str
+ choices: [WGS84, NAD83, MLLW]
+ running_config:
+ description: Used only with state C(parsed).
+ type: str
+ state:
+ description:
+ - C(merged) - Merge config with existing LLDP interface settings.
+ - C(replaced) - Replace config for listed interfaces.
+ - C(overridden) - Replace config for all LLDP interfaces.
+ - C(deleted) - Remove listed or all LLDP interface config.
+ - C(gathered) - Read LLDP interface config from device without changes.
+ - C(rendered) - Return commands for provided config without connecting.
+ - C(parsed) - Parse running_config into structured data.
+ type: str
+ choices: [merged, replaced, overridden, deleted, gathered, rendered, parsed]
+ default: merged
+notes:
+ - Targets VyOS 1.5+ exclusively. The C(mode) parameter replaces the C(enable)
+ boolean used in C(vyos.vyos.vyos_lldp_interfaces) for VyOS 1.3/1.4.
+seealso:
+ - module: vyos.vyos.vyos_lldp_interfaces
+ - module: vyos.rest.vyos_lldp_global
+"""
+
+EXAMPLES = r"""
+- name: Merge LLDP interface configuration
+ vyos.rest.vyos_lldp_interfaces:
+ config:
+ - name: eth0
+ mode: disable
+ location:
+ elin: "1234567890"
+ - name: eth1
+ location:
+ coordinate_based:
+ latitude: "33.524449N"
+ longitude: "22.267255E"
+ altitude: 2200
+ datum: WGS84
+ state: merged
+
+- name: Delete all LLDP interface configuration
+ vyos.rest.vyos_lldp_interfaces:
+ state: deleted
+
+- name: Gather current LLDP interface configuration
+ vyos.rest.vyos_lldp_interfaces:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: LLDP interface configuration before this module ran.
+ returned: always
+ type: list
+after:
+ description: LLDP 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 LLDP interface configuration as structured data.
+ returned: when state is gathered
+ type: list
+rendered:
+ description: Commands for provided config (state=rendered).
+ returned: when state is rendered
+ type: list
+parsed:
+ description: Structured data parsed from running_config (state=parsed).
+ returned: when state is parsed
+ 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
+
+
+_BASE = ["service", "lldp", "interface"]
+
+
+def _iface_base(name):
+ return _BASE + [name]
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(["service", "lldp"])
+ if not raw or not isinstance(raw, dict):
+ return []
+
+ iface_data = raw.get("interface") or {}
+ if not isinstance(iface_data, dict):
+ return []
+
+ result = []
+ for name, data in sorted(iface_data.items()):
+ data = data or {}
+ entry = {"name": name}
+
+ if data.get("mode"):
+ entry["mode"] = data["mode"]
+
+ loc_data = data.get("location") or {}
+ if isinstance(loc_data, dict) and loc_data:
+ loc = {}
+ if "elin" in loc_data:
+ loc["elin"] = loc_data["elin"]
+ cb = loc_data.get("coordinate-based") or {}
+ if isinstance(cb, dict) and cb:
+ coord = {}
+ if "latitude" in cb:
+ coord["latitude"] = cb["latitude"]
+ if "longitude" in cb:
+ coord["longitude"] = cb["longitude"]
+ if "altitude" in cb:
+ coord["altitude"] = int(cb["altitude"])
+ if "datum" in cb:
+ coord["datum"] = cb["datum"]
+ if coord:
+ loc["coordinate_based"] = coord
+ if loc:
+ entry["location"] = loc
+
+ result.append(entry)
+
+ return result
+
+
+def _normalize(config):
+ result = {}
+ for entry in config or []:
+ name = entry["name"]
+ loc = entry.get("location") or {}
+ cb = loc.get("coordinate_based") or {}
+ result[name] = {
+ "mode": entry.get("mode"),
+ "elin": loc.get("elin"),
+ "latitude": cb.get("latitude"),
+ "longitude": cb.get("longitude"),
+ "altitude": cb.get("altitude"),
+ "datum": cb.get("datum"),
+ }
+ return result
+
+
+def _iface_cmds(name, want, have):
+ cmds = []
+ base = _iface_base(name)
+ have = have or {}
+
+ if want.get("mode") and want["mode"] != have.get("mode"):
+ cmds.append(("set", base + ["mode", want["mode"]]))
+ elif not want.get("mode") and have.get("mode"):
+ cmds.append(("delete", base + ["mode"]))
+
+ loc_base = base + ["location"]
+ if want.get("elin") and want["elin"] != have.get("elin"):
+ cmds.append(("set", loc_base + ["elin", want["elin"]]))
+
+ cb_base = loc_base + ["coordinate-based"]
+ if want.get("latitude") and want["latitude"] != have.get("latitude"):
+ cmds.append(("set", cb_base + ["latitude", want["latitude"]]))
+ if want.get("longitude") and want["longitude"] != have.get("longitude"):
+ cmds.append(("set", cb_base + ["longitude", want["longitude"]]))
+ if want.get("altitude") is not None and want["altitude"] != have.get("altitude"):
+ cmds.append(("set", cb_base + ["altitude", str(want["altitude"])]))
+ if want.get("datum") and want["datum"] != have.get("datum"):
+ cmds.append(("set", cb_base + ["datum", want["datum"]]))
+
+ return cmds
+
+
+def build_commands(config, have_raw, state):
+ cmds = []
+ have_map = _normalize(have_raw)
+
+ if state == "deleted":
+ if not config:
+ for name in have_map:
+ cmds.append(("delete", _iface_base(name)))
+ else:
+ want_map = _normalize(config)
+ for name in want_map:
+ if name in have_map:
+ cmds.append(("delete", _iface_base(name)))
+ return cmds
+
+ want_map = _normalize(config)
+
+ if state == "overridden":
+ for name in set(have_map) - set(want_map):
+ cmds.append(("delete", _iface_base(name)))
+
+ for name, want in want_map.items():
+ have = have_map.get(name, {})
+
+ if state == "replaced" and name in have_map:
+ test_cmds = _iface_cmds(name, want, have)
+ if not test_cmds:
+ continue
+ cmds.append(("delete", _iface_base(name)))
+ have = {}
+
+ cmds += _iface_cmds(name, want, have)
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ name=dict(type="str", required=True),
+ mode=dict(type="str", choices=["disable", "rx-tx", "rx", "tx"]),
+ location=dict(
+ type="dict",
+ options=dict(
+ elin=dict(type="str"),
+ coordinate_based=dict(
+ type="dict",
+ options=dict(
+ latitude=dict(type="str", required=True),
+ longitude=dict(type="str", required=True),
+ altitude=dict(type="int"),
+ datum=dict(type="str", choices=["WGS84", "NAD83", "MLLW"]),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ running_config=dict(type="str"),
+ state=dict(
+ type="str",
+ default="merged",
+ choices=["merged", "replaced", "overridden", "deleted", "gathered", "rendered", "parsed"],
+ ),
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=ARGUMENT_SPEC,
+ mutually_exclusive=[["config", "running_config"]],
+ required_if=[
+ ("state", "rendered", ["config"]),
+ ("state", "parsed", ["running_config"]),
+ ],
+ supports_check_mode=True,
+ )
+ vyos = VyOSModule(module)
+
+ state = module.params["state"]
+ config = module.params.get("config") or []
+
+ if state == "parsed":
+ module.exit_json(parsed=[])
+
+ if state == "rendered":
+ cmds = build_commands(config, [], "merged")
+ module.exit_json(rendered=cmds, commands=cmds)
+
+ 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/plugins/modules/vyos_logging_global.py b/plugins/modules/vyos_logging_global.py
index 09fdd6b..f18ba3f 100644
--- a/plugins/modules/vyos_logging_global.py
+++ b/plugins/modules/vyos_logging_global.py
@@ -305,7 +305,7 @@ def normalize_running(raw):
for f, data in raw.get("console", {}).get("facility", {}).items():
result["console"]["facilities"][f] = data.get("level")
- g = raw.get("global", {})
+ g = raw.get("local", {})
for f, data in g.get("facility", {}).items():
result["global"]["facilities"][f] = data.get("level")
if "archive" in g:
@@ -315,7 +315,7 @@ def normalize_running(raw):
if "preserve-fqdn" in g:
result["global"]["preserve_fqdn"] = True
- for host, data in raw.get("host", {}).items():
+ for host, data in raw.get("remote", {}).items():
h = {"port": data.get("port"), "facilities": {}}
for f, fd in data.get("facility", {}).items():
h["facilities"][f] = {
@@ -402,7 +402,7 @@ def build_commands(want, have, state):
)
cmds += diff_facilities(
- ["system", "syslog", "global"],
+ ["system", "syslog", "local"],
want["global"]["facilities"],
have["global"]["facilities"],
state,
@@ -416,7 +416,7 @@ def build_commands(want, have, state):
)
cmds += diff_map(
- ["system", "syslog", "host"],
+ ["system", "syslog", "remote"],
want["hosts"],
have["hosts"],
state,
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()
diff --git a/plugins/modules/vyos_ospfv2.py b/plugins/modules/vyos_ospfv2.py
new file mode 100644
index 0000000..5ac333d
--- /dev/null
+++ b/plugins/modules/vyos_ospfv2.py
@@ -0,0 +1,955 @@
+#!/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_ospfv2
+short_description: Manage OSPFv2 configuration on VyOS devices using REST API
+description:
+ - Manages OSPFv2 configuration on VyOS devices via the REST API.
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+ - 'In VyOS 1.5+, passive interfaces use per-interface config rather than passive-interface.'
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: OSPFv2 configuration.
+ type: dict
+ suboptions:
+ areas:
+ description: OSPFv2 areas.
+ type: list
+ elements: dict
+ suboptions:
+ area_id:
+ description: Area ID.
+ type: str
+ required: true
+ area_type:
+ description: Area type.
+ type: dict
+ suboptions:
+ normal:
+ description: Normal area.
+ type: bool
+ nssa:
+ description: NSSA area.
+ type: dict
+ suboptions:
+ set:
+ description: Enable NSSA.
+ type: bool
+ default_cost:
+ description: Default cost for NSSA.
+ type: int
+ no_summary:
+ description: Do not inject inter-area routes.
+ type: bool
+ translate:
+ description: NSSA-ABR translate setting.
+ type: str
+ choices: [always, candidate, never]
+ stub:
+ description: Stub area.
+ type: dict
+ suboptions:
+ set:
+ description: Enable stub.
+ type: bool
+ default_cost:
+ description: Default cost for stub.
+ type: int
+ no_summary:
+ description: Do not inject inter-area routes.
+ type: bool
+ authentication:
+ description: Area authentication type.
+ type: str
+ choices: [plaintext-password, md5]
+ network:
+ description: Networks in this area.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: Network address.
+ type: str
+ required: true
+ range:
+ description: Area ranges.
+ type: list
+ elements: dict
+ suboptions:
+ address:
+ description: Range address.
+ type: str
+ required: true
+ cost:
+ description: Cost for this range.
+ type: int
+ not_advertise:
+ description: Do not advertise this range.
+ type: bool
+ substitute:
+ description: Substitute prefix.
+ type: str
+ shortcut:
+ description: Shortcut mode.
+ type: str
+ choices: [default, disable, enable]
+ auto_cost:
+ description: Auto-cost reference bandwidth.
+ type: dict
+ suboptions:
+ reference_bandwidth:
+ description: Reference bandwidth in Mbps.
+ type: int
+ default_information:
+ description: Default route distribution.
+ type: dict
+ suboptions:
+ originate:
+ description: Originate default route.
+ type: dict
+ suboptions:
+ always:
+ description: Always advertise default route.
+ type: bool
+ metric:
+ description: Metric for default route.
+ type: int
+ metric_type:
+ description: Metric type.
+ type: int
+ route_map:
+ description: Route map.
+ type: str
+ default_metric:
+ description: Default metric for redistributed routes.
+ type: int
+ distance:
+ description: Administrative distances.
+ type: dict
+ suboptions:
+ global:
+ description: Global OSPFv2 distance.
+ type: int
+ ospf:
+ description: Per-route-type distances.
+ type: dict
+ suboptions:
+ external:
+ description: External route distance.
+ type: int
+ inter_area:
+ description: Inter-area route distance.
+ type: int
+ intra_area:
+ description: Intra-area route distance.
+ type: int
+ log_adjacency_changes:
+ description: Log adjacency changes.
+ type: str
+ choices: [detail]
+ neighbor:
+ description: OSPF neighbors.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_id:
+ description: Neighbor IP.
+ type: str
+ required: true
+ poll_interval:
+ description: Poll interval.
+ type: int
+ priority:
+ description: Neighbor priority.
+ type: int
+ parameters:
+ description: OSPFv2 parameters.
+ type: dict
+ suboptions:
+ abr_type:
+ description: ABR type.
+ type: str
+ choices: [cisco, ibm, shortcut, standard]
+ opaque_lsa:
+ description: Enable opaque LSA.
+ type: bool
+ rfc1583_compatibility:
+ description: Enable RFC1583 compatibility.
+ type: bool
+ router_id:
+ description: Router ID.
+ type: str
+ passive_interface:
+ description: >
+ Passive interfaces (VyOS 1.5+: configured via
+ C(protocols ospf interface <name> passive)).
+ type: list
+ elements: str
+ redistribute:
+ description: Route redistribution.
+ type: list
+ elements: dict
+ suboptions:
+ route_type:
+ description: Protocol to redistribute.
+ type: str
+ choices: [bgp, connected, kernel, rip, static]
+ metric:
+ description: Metric.
+ type: int
+ metric_type:
+ description: Metric type.
+ type: int
+ route_map:
+ description: Route map.
+ type: str
+ state:
+ description:
+ - Desired state.
+ - C(merged) adds/updates without removing existing config.
+ - C(replaced) replaces the entire OSPFv2 configuration.
+ - C(deleted) removes OSPFv2 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).
+ - VyOS 1.5+ uses per-interface passive configuration rather than
+ the global C(passive-interface) command used in VyOS 1.4.
+"""
+
+EXAMPLES = r"""
+- name: Merge OSPFv2 configuration
+ vyos.rest.vyos_ospfv2:
+ config:
+ parameters:
+ router_id: 192.0.1.1
+ abr_type: cisco
+ auto_cost:
+ reference_bandwidth: 2
+ areas:
+ - area_id: "2"
+ area_type:
+ normal: true
+ network:
+ - address: 192.0.2.0/24
+ - area_id: "3"
+ area_type:
+ nssa:
+ set: true
+ - area_id: "4"
+ area_type:
+ stub:
+ default_cost: 20
+ range:
+ - address: 192.0.3.0/24
+ cost: 10
+ redistribute:
+ - route_type: bgp
+ metric: 10
+ passive_interface:
+ - eth1
+ state: merged
+
+- name: Delete all OSPFv2 configuration
+ vyos.rest.vyos_ospfv2:
+ state: deleted
+
+- name: Gather current OSPFv2 configuration
+ vyos.rest.vyos_ospfv2:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: OSPFv2 configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: OSPFv2 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 OSPFv2 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", "ospf"]
+
+
+def _parse_areas(raw_areas):
+ if not raw_areas or not isinstance(raw_areas, dict):
+ return []
+ areas = []
+ for area_id, data in sorted(raw_areas.items()):
+ area = {"area_id": area_id}
+ data = data or {}
+
+ # area-type
+ at = data.get("area-type", {})
+ if at:
+ area_type = {}
+ if "normal" in at:
+ area_type["normal"] = True
+ if "nssa" in at:
+ nssa_data = at["nssa"] or {}
+ nssa = {"set": True}
+ if "default-cost" in nssa_data:
+ nssa["default_cost"] = int(nssa_data["default-cost"])
+ if "no-summary" in nssa_data:
+ nssa["no_summary"] = True
+ if "translate" in nssa_data:
+ nssa["translate"] = nssa_data["translate"]
+ area_type["nssa"] = nssa
+ if "stub" in at:
+ stub_data = at["stub"] or {}
+ stub = {"set": True}
+ if "default-cost" in stub_data:
+ stub["default_cost"] = int(stub_data["default-cost"])
+ if "no-summary" in stub_data:
+ stub["no_summary"] = True
+ area_type["stub"] = stub
+ if area_type:
+ area["area_type"] = area_type
+
+ if "authentication" in data:
+ area["authentication"] = data["authentication"]
+
+ if "shortcut" in data:
+ area["shortcut"] = data["shortcut"]
+
+ # network
+ net = data.get("network")
+ if net:
+ if isinstance(net, str):
+ area["network"] = [{"address": net}]
+ elif isinstance(net, dict):
+ area["network"] = [{"address": a} for a in sorted(net.keys())]
+ elif isinstance(net, list):
+ area["network"] = [{"address": a} for a in sorted(net)]
+
+ # range
+ rng = data.get("range", {})
+ if rng and isinstance(rng, dict):
+ ranges = []
+ for addr, rdata in sorted(rng.items()):
+ r = {"address": addr}
+ rdata = rdata or {}
+ if "cost" in rdata:
+ r["cost"] = int(rdata["cost"])
+ if "not-advertise" in rdata:
+ r["not_advertise"] = True
+ if "substitute" in rdata:
+ r["substitute"] = rdata["substitute"]
+ ranges.append(r)
+ if ranges:
+ area["range"] = ranges
+
+ areas.append(area)
+ return areas
+
+
+def _parse_redistribute(raw):
+ if not raw or not isinstance(raw, dict):
+ return []
+ result = []
+ for rt, data in sorted(raw.items()):
+ entry = {"route_type": rt}
+ data = data or {}
+ if "metric" in data:
+ entry["metric"] = int(data["metric"])
+ if "metric-type" in data:
+ entry["metric_type"] = int(data["metric-type"])
+ if "route-map" in data:
+ entry["route_map"] = data["route-map"]
+ result.append(entry)
+ return result
+
+
+def _parse_neighbor(raw):
+ if not raw or not isinstance(raw, dict):
+ return []
+ result = []
+ for nb_id, data in sorted(raw.items()):
+ entry = {"neighbor_id": nb_id}
+ data = data or {}
+ if "poll-interval" in data:
+ entry["poll_interval"] = int(data["poll-interval"])
+ if "priority" in data:
+ entry["priority"] = int(data["priority"])
+ result.append(entry)
+ return result
+
+
+def _parse_parameters(raw):
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+ if "router-id" in raw:
+ result["router_id"] = raw["router-id"]
+ if "abr-type" in raw:
+ result["abr_type"] = raw["abr-type"]
+ if "opaque-lsa" in raw:
+ result["opaque_lsa"] = True
+ if "rfc1583-compatibility" in raw:
+ result["rfc1583_compatibility"] = True
+ return result
+
+
+def _parse_default_information(raw):
+ if not raw or not isinstance(raw, dict):
+ return {}
+ orig = raw.get("originate", {}) or {}
+ result = {}
+ if "always" in orig:
+ result["always"] = True
+ if "metric" in orig:
+ result["metric"] = int(orig["metric"])
+ if "metric-type" in orig:
+ result["metric_type"] = int(orig["metric-type"])
+ if "route-map" in orig:
+ result["route_map"] = orig["route-map"]
+ if result:
+ return {"originate": result}
+ return {}
+
+
+def _parse_distance(raw):
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+ if "global" in raw:
+ result["global"] = int(raw["global"])
+ ospf = raw.get("ospf", {}) or {}
+ if ospf:
+ od = {}
+ if "external" in ospf:
+ od["external"] = int(ospf["external"])
+ if "inter-area" in ospf:
+ od["inter_area"] = int(ospf["inter-area"])
+ if "intra-area" in ospf:
+ od["intra_area"] = int(ospf["intra-area"])
+ if od:
+ result["ospf"] = od
+ return result
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+
+ areas = _parse_areas(raw.get("area"))
+ if areas:
+ result["areas"] = areas
+
+ ac = raw.get("auto-cost", {})
+ if ac and "reference-bandwidth" in ac:
+ result["auto_cost"] = {"reference_bandwidth": int(ac["reference-bandwidth"])}
+
+ di = _parse_default_information(raw.get("default-information", {}))
+ if di:
+ result["default_information"] = di
+
+ if "default-metric" in raw:
+ result["default_metric"] = int(raw["default-metric"])
+
+ dist = _parse_distance(raw.get("distance", {}))
+ if dist:
+ result["distance"] = dist
+
+ lac = raw.get("log-adjacency-changes", {})
+ if lac:
+ if isinstance(lac, dict) and "detail" in lac:
+ result["log_adjacency_changes"] = "detail"
+ elif lac == "detail":
+ result["log_adjacency_changes"] = "detail"
+
+ neighbors = _parse_neighbor(raw.get("neighbor"))
+ if neighbors:
+ result["neighbor"] = neighbors
+
+ params = _parse_parameters(raw.get("parameters"))
+ if params:
+ result["parameters"] = params
+
+ # passive interfaces — VyOS 1.5 uses interface <name> passive
+ iface_raw = raw.get("interface", {}) or {}
+ passive = sorted(
+ [name for name, data in iface_raw.items() if isinstance(data, dict) and "passive" in data],
+ )
+ if passive:
+ result["passive_interface"] = passive
+
+ redist = _parse_redistribute(raw.get("redistribute"))
+ if redist:
+ result["redistribute"] = redist
+
+ return result
+
+
+def _area_type_cmds(abase, area_type, have_at):
+ cmds = []
+ have_at = have_at or {}
+ if area_type.get("normal") and not have_at.get("normal"):
+ cmds.append(("set", abase + ["area-type", "normal"]))
+ nssa = area_type.get("nssa") or {}
+ if nssa:
+ have_nssa = have_at.get("nssa") or {}
+ if not have_nssa:
+ cmds.append(("set", abase + ["area-type", "nssa"]))
+ if nssa.get("default_cost") and nssa["default_cost"] != have_nssa.get("default_cost"):
+ cmds.append(
+ (
+ "set",
+ abase
+ + [
+ "area-type",
+ "nssa",
+ "default-cost",
+ str(nssa["default_cost"]),
+ ],
+ ),
+ )
+ if nssa.get("no_summary") and not have_nssa.get("no_summary"):
+ cmds.append(("set", abase + ["area-type", "nssa", "no-summary"]))
+ if nssa.get("translate") and nssa["translate"] != have_nssa.get("translate"):
+ cmds.append(("set", abase + ["area-type", "nssa", "translate", nssa["translate"]]))
+ stub = area_type.get("stub") or {}
+ if stub:
+ have_stub = have_at.get("stub") or {}
+ if not have_stub:
+ if stub.get("default_cost"):
+ cmds.append(
+ (
+ "set",
+ abase
+ + [
+ "area-type",
+ "stub",
+ "default-cost",
+ str(stub["default_cost"]),
+ ],
+ ),
+ )
+ else:
+ cmds.append(("set", abase + ["area-type", "stub"]))
+ elif stub.get("default_cost") and stub["default_cost"] != have_stub.get("default_cost"):
+ cmds.append(
+ (
+ "set",
+ abase
+ + [
+ "area-type",
+ "stub",
+ "default-cost",
+ str(stub["default_cost"]),
+ ],
+ ),
+ )
+ return cmds
+
+
+def _area_cmds(area, have_area):
+ cmds = []
+ area_id = area["area_id"]
+ abase = _BASE + ["area", area_id]
+ have_area = have_area or {}
+
+ if area.get("area_type"):
+ cmds += _area_type_cmds(abase, area["area_type"], have_area.get("area_type"))
+
+ if area.get("authentication") and area["authentication"] != have_area.get("authentication"):
+ cmds.append(("set", abase + ["authentication", area["authentication"]]))
+
+ if area.get("shortcut") and area["shortcut"] != have_area.get("shortcut"):
+ cmds.append(("set", abase + ["shortcut", area["shortcut"]]))
+
+ want_nets = {n["address"] for n in (area.get("network") or [])}
+ have_nets = {n["address"] for n in (have_area.get("network") or [])}
+ for addr in want_nets - have_nets:
+ cmds.append(("set", abase + ["network", addr]))
+
+ want_ranges = {r["address"]: r for r in (area.get("range") or [])}
+ have_ranges = {r["address"]: r for r in (have_area.get("range") or [])}
+ for addr, rng in want_ranges.items():
+ have_rng = have_ranges.get(addr, {})
+ if addr not in have_ranges:
+ cmds.append(("set", abase + ["range", addr]))
+ if rng.get("cost") and rng["cost"] != have_rng.get("cost"):
+ cmds.append(("set", abase + ["range", addr, "cost", str(rng["cost"])]))
+ if rng.get("not_advertise") and not have_rng.get("not_advertise"):
+ cmds.append(("set", abase + ["range", addr, "not-advertise"]))
+ if rng.get("substitute") and rng["substitute"] != have_rng.get("substitute"):
+ cmds.append(("set", abase + ["range", addr, "substitute", rng["substitute"]]))
+
+ return cmds
+
+
+def _parameters_cmds(params, have_params):
+ cmds = []
+ have_params = have_params or {}
+ pbase = _BASE + ["parameters"]
+ if params.get("router_id") and params["router_id"] != have_params.get("router_id"):
+ cmds.append(("set", pbase + ["router-id", params["router_id"]]))
+ if params.get("abr_type") and params["abr_type"] != have_params.get("abr_type"):
+ cmds.append(("set", pbase + ["abr-type", params["abr_type"]]))
+ if params.get("opaque_lsa") and not have_params.get("opaque_lsa"):
+ cmds.append(("set", pbase + ["opaque-lsa"]))
+ if params.get("rfc1583_compatibility") and not have_params.get("rfc1583_compatibility"):
+ cmds.append(("set", pbase + ["rfc1583-compatibility"]))
+ return cmds
+
+
+def _redistribute_cmds(redist_list, have_redist_list):
+ cmds = []
+ want = {r["route_type"]: r for r in (redist_list or [])}
+ have = {r["route_type"]: r for r in (have_redist_list or [])}
+ for rt, entry in want.items():
+ have_entry = have.get(rt, {})
+ rbase = _BASE + ["redistribute", rt]
+ if rt not in have:
+ cmds.append(("set", rbase))
+ if entry.get("metric") and entry["metric"] != have_entry.get("metric"):
+ cmds.append(("set", rbase + ["metric", str(entry["metric"])]))
+ if entry.get("metric_type") and entry["metric_type"] != have_entry.get("metric_type"):
+ cmds.append(("set", rbase + ["metric-type", str(entry["metric_type"])]))
+ if entry.get("route_map") and entry["route_map"] != have_entry.get("route_map"):
+ cmds.append(("set", rbase + ["route-map", entry["route_map"]]))
+ return cmds
+
+
+def _neighbor_cmds(neighbors, have_neighbors):
+ cmds = []
+ want = {n["neighbor_id"]: n for n in (neighbors or [])}
+ have = {n["neighbor_id"]: n for n in (have_neighbors or [])}
+ for nb_id, entry in want.items():
+ have_entry = have.get(nb_id, {})
+ nbase = _BASE + ["neighbor", nb_id]
+ if nb_id not in have:
+ cmds.append(("set", nbase))
+ if entry.get("priority") and entry["priority"] != have_entry.get("priority"):
+ cmds.append(("set", nbase + ["priority", str(entry["priority"])]))
+ if entry.get("poll_interval") and entry["poll_interval"] != have_entry.get("poll_interval"):
+ cmds.append(("set", nbase + ["poll-interval", str(entry["poll_interval"])]))
+ return cmds
+
+
+def _default_info_cmds(di, have_di):
+ cmds = []
+ have_di = have_di or {}
+ orig = (di or {}).get("originate") or {}
+ have_orig = have_di.get("originate") or {}
+ if not orig:
+ return cmds
+ dbase = _BASE + ["default-information", "originate"]
+ if orig.get("always") and not have_orig.get("always"):
+ cmds.append(("set", dbase + ["always"]))
+ if orig.get("metric") and orig["metric"] != have_orig.get("metric"):
+ cmds.append(("set", dbase + ["metric", str(orig["metric"])]))
+ if orig.get("metric_type") and orig["metric_type"] != have_orig.get("metric_type"):
+ cmds.append(("set", dbase + ["metric-type", str(orig["metric_type"])]))
+ if orig.get("route_map") and orig["route_map"] != have_orig.get("route_map"):
+ cmds.append(("set", dbase + ["route-map", orig["route_map"]]))
+ return cmds
+
+
+def build_commands(config, have, state):
+ cmds = []
+
+ if state == "deleted":
+ if have:
+ cmds.append(("delete", _BASE))
+ return cmds
+
+ if state == "replaced":
+ 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 = {}
+
+ config = config or {}
+
+ # parameters
+ if config.get("parameters"):
+ cmds += _parameters_cmds(config["parameters"], have.get("parameters"))
+
+ # auto_cost
+ ac = config.get("auto_cost") or {}
+ have_ac = have.get("auto_cost") or {}
+ if ac.get("reference_bandwidth") and ac["reference_bandwidth"] != have_ac.get(
+ "reference_bandwidth",
+ ):
+ cmds.append(
+ (
+ "set",
+ _BASE
+ + [
+ "auto-cost",
+ "reference-bandwidth",
+ str(ac["reference_bandwidth"]),
+ ],
+ ),
+ )
+
+ # default_information
+ if config.get("default_information"):
+ cmds += _default_info_cmds(
+ config["default_information"],
+ have.get("default_information"),
+ )
+
+ # default_metric
+ if config.get("default_metric") and config["default_metric"] != have.get("default_metric"):
+ cmds.append(("set", _BASE + ["default-metric", str(config["default_metric"])]))
+
+ # distance
+ dist = config.get("distance") or {}
+ have_dist = have.get("distance") or {}
+ if dist.get("global") and dist["global"] != have_dist.get("global"):
+ cmds.append(("set", _BASE + ["distance", "global", str(dist["global"])]))
+ ospf_dist = dist.get("ospf") or {}
+ have_ospf_dist = have_dist.get("ospf") or {}
+ for key, api_key in [
+ ("external", "external"),
+ ("inter_area", "inter-area"),
+ ("intra_area", "intra-area"),
+ ]:
+ if ospf_dist.get(key) and ospf_dist[key] != have_ospf_dist.get(key):
+ cmds.append(("set", _BASE + ["distance", "ospf", api_key, str(ospf_dist[key])]))
+
+ # log_adjacency_changes
+ if config.get("log_adjacency_changes") and config["log_adjacency_changes"] != have.get(
+ "log_adjacency_changes",
+ ):
+ cmds.append(("set", _BASE + ["log-adjacency-changes", config["log_adjacency_changes"]]))
+
+ # neighbor
+ if config.get("neighbor"):
+ cmds += _neighbor_cmds(config["neighbor"], have.get("neighbor"))
+
+ # redistribute
+ if config.get("redistribute"):
+ cmds += _redistribute_cmds(config["redistribute"], have.get("redistribute"))
+
+ # passive_interface — VyOS 1.5 per-interface style
+ want_passive = set(config.get("passive_interface") or [])
+ have_passive = set(have.get("passive_interface") or [])
+ for iface in want_passive - have_passive:
+ cmds.append(("set", _BASE + ["interface", iface, "passive"]))
+
+ # areas
+ have_areas = {a["area_id"]: a for a in (have.get("areas") or [])}
+ for area in config.get("areas") or []:
+ have_area = have_areas.get(area["area_id"], {})
+ cmds += _area_cmds(area, have_area)
+
+ 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),
+ area_type=dict(
+ type="dict",
+ options=dict(
+ normal=dict(type="bool"),
+ nssa=dict(
+ type="dict",
+ options=dict(
+ set=dict(type="bool"),
+ default_cost=dict(type="int"),
+ no_summary=dict(type="bool"),
+ translate=dict(
+ type="str",
+ choices=["always", "candidate", "never"],
+ ),
+ ),
+ ),
+ stub=dict(
+ type="dict",
+ options=dict(
+ set=dict(type="bool"),
+ default_cost=dict(type="int"),
+ no_summary=dict(type="bool"),
+ ),
+ ),
+ ),
+ ),
+ authentication=dict(
+ type="str",
+ choices=["plaintext-password", "md5"],
+ ),
+ network=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str", required=True),
+ ),
+ ),
+ range=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ address=dict(type="str", required=True),
+ cost=dict(type="int"),
+ not_advertise=dict(type="bool"),
+ substitute=dict(type="str"),
+ ),
+ ),
+ shortcut=dict(type="str", choices=["default", "disable", "enable"]),
+ ),
+ ),
+ auto_cost=dict(
+ type="dict",
+ options=dict(
+ reference_bandwidth=dict(type="int"),
+ ),
+ ),
+ default_information=dict(
+ type="dict",
+ options=dict(
+ originate=dict(
+ type="dict",
+ options=dict(
+ always=dict(type="bool"),
+ metric=dict(type="int"),
+ metric_type=dict(type="int"),
+ route_map=dict(type="str"),
+ ),
+ ),
+ ),
+ ),
+ default_metric=dict(type="int"),
+ distance=dict(
+ type="dict",
+ options=dict(
+ **{"global": dict(type="int")},
+ ospf=dict(
+ type="dict",
+ options=dict(
+ external=dict(type="int"),
+ inter_area=dict(type="int"),
+ intra_area=dict(type="int"),
+ ),
+ ),
+ ),
+ ),
+ log_adjacency_changes=dict(type="str", choices=["detail"]),
+ neighbor=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_id=dict(type="str", required=True),
+ poll_interval=dict(type="int"),
+ priority=dict(type="int"),
+ ),
+ ),
+ parameters=dict(
+ type="dict",
+ options=dict(
+ abr_type=dict(
+ type="str",
+ choices=["cisco", "ibm", "shortcut", "standard"],
+ ),
+ opaque_lsa=dict(type="bool"),
+ rfc1583_compatibility=dict(type="bool"),
+ router_id=dict(type="str"),
+ ),
+ ),
+ passive_interface=dict(type="list", elements="str"),
+ redistribute=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ route_type=dict(
+ type="str",
+ choices=["bgp", "connected", "kernel", "rip", "static"],
+ ),
+ metric=dict(type="int"),
+ metric_type=dict(type="int"),
+ 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/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/plugins/modules/vyos_route_maps.py b/plugins/modules/vyos_route_maps.py
index 55b1706..607d1ab 100644
--- a/plugins/modules/vyos_route_maps.py
+++ b/plugins/modules/vyos_route_maps.py
@@ -308,7 +308,7 @@ def _want_to_api_match(match):
return api
-def _rule_cmds(rm_name, rule, have_rule):
+def _rule_cmds(rm_name, rule, have_rule, state="merged"):
cmds = []
seq = str(rule["sequence"])
rbase = _BASE + [rm_name, "rule", seq]
@@ -343,8 +343,13 @@ def _rule_cmds(rm_name, rule, have_rule):
want_set_api = _want_to_api_set(rule.get("set"))
have_set = have_rule.get("set") or {}
- if want_set_api != have_set:
- cmds += _set_cmds(rbase, rule.get("set"))
+ if state in ("replaced", "overridden"):
+ if want_set_api != have_set:
+ cmds += _set_cmds(rbase, rule.get("set"))
+ else:
+ have_subset = {k: have_set[k] for k in want_set_api if k in have_set}
+ if want_set_api != have_subset:
+ cmds += _set_cmds(rbase, rule.get("set"))
return cmds
@@ -380,7 +385,7 @@ def build_commands(config, have_raw, state):
test_cmds = []
for rule in rm.get("entries") or []:
have_rule = have_entries.get(str(rule["sequence"]), {})
- test_cmds += _rule_cmds(rm_name, rule, have_rule)
+ test_cmds += _rule_cmds(rm_name, rule, have_rule, state)
if test_cmds or extra_seqs:
cmds.append(("delete", _BASE + [rm_name]))
have_rm = {}
@@ -391,7 +396,7 @@ def build_commands(config, have_raw, state):
for rule in rm.get("entries") or []:
have_rule = have_entries.get(str(rule["sequence"]), {})
- cmds += _rule_cmds(rm_name, rule, have_rule)
+ cmds += _rule_cmds(rm_name, rule, have_rule, state)
return cmds
diff --git a/plugins/modules/vyos_snmp_server.py b/plugins/modules/vyos_snmp_server.py
index 3b40344..1079402 100644
--- a/plugins/modules/vyos_snmp_server.py
+++ b/plugins/modules/vyos_snmp_server.py
@@ -525,12 +525,12 @@ def _build_scalar_commands(want, have, state):
want_val = want.get(argspec_key)
have_val = have.get(argspec_key)
path = SNMP_BASE + [api_key]
- if state in ("merged", "replaced", "overridden"):
- if want_val and want_val != have_val:
- cmds.append(_set(path + [want_val]))
if state in ("replaced", "overridden"):
if have_val and want_val != have_val:
cmds.append(_delete(path))
+ if state in ("merged", "replaced", "overridden"):
+ if want_val and want_val != have_val:
+ cmds.append(_set(path + [want_val]))
return cmds
@@ -547,10 +547,10 @@ def _build_community_commands(want_list, have_list, state):
base = SNMP_BASE + ["community", name]
want_auth = want_comm.get("authorization_type")
have_auth = have_comm.get("authorization_type")
- if want_auth and want_auth != have_auth:
- cmds.append(_set(base + ["authorization", want_auth]))
if state in ("replaced", "overridden") and have_auth and want_auth != have_auth:
cmds.append(_delete(base + ["authorization"]))
+ if want_auth and want_auth != have_auth:
+ cmds.append(_set(base + ["authorization", want_auth]))
want_clients = set(want_comm.get("clients") or [])
have_clients = set(have_comm.get("clients") or [])
for c in want_clients - have_clients: