summaryrefslogtreecommitdiff
path: root/plugins/modules
diff options
context:
space:
mode:
authoromnom62 <omnom62@outlook.com>2026-07-01 12:56:25 +1000
committeromnom62 <omnom62@outlook.com>2026-07-01 12:56:25 +1000
commitaad7d2468a25650b7b6aa21ea9cf0757bd6ef6de (patch)
tree68a03096006256249e08380c8fa94c1f4c3f2e0a /plugins/modules
parenta2c9a7b8cd03e34f2b50d925901b8a56a898a182 (diff)
downloadrest.vyos-aad7d2468a25650b7b6aa21ea9cf0757bd6ef6de.tar.gz
rest.vyos-aad7d2468a25650b7b6aa21ea9cf0757bd6ef6de.zip
T8989: Wave 3 vyos_bgp_global module with integration and unit tests
Diffstat (limited to 'plugins/modules')
-rw-r--r--plugins/modules/vyos_bgp_global.py572
1 files changed, 572 insertions, 0 deletions
diff --git a/plugins/modules/vyos_bgp_global.py b/plugins/modules/vyos_bgp_global.py
new file mode 100644
index 0000000..a05e3b0
--- /dev/null
+++ b/plugins/modules/vyos_bgp_global.py
@@ -0,0 +1,572 @@
+#!/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_bgp_global
+short_description: Manage BGP global configuration on VyOS devices using REST API
+description:
+ - Manages BGP global configuration on VyOS devices via the REST API.
+ - Covers system AS, parameters, neighbors, and peer-groups.
+ - For per-neighbor address-family configuration use M(vyos.rest.vyos_bgp_address_family).
+ - Uses REST API (C(connection=httpapi)) instead of CLI.
+version_added: "1.0.0"
+author:
+ - VyOS Community (@vyos)
+options:
+ config:
+ description: BGP global configuration.
+ type: dict
+ suboptions:
+ as_number:
+ description: BGP autonomous system number.
+ type: int
+ required: true
+ parameters:
+ description: BGP global parameters.
+ type: dict
+ suboptions:
+ router_id:
+ description: BGP router ID.
+ type: str
+ confederation:
+ description: AS confederation parameters.
+ type: dict
+ suboptions:
+ identifier:
+ description: Confederation AS identifier.
+ type: int
+ peers:
+ description: Peer ASs in confederation.
+ type: list
+ elements: int
+ bestpath:
+ description: BGP bestpath parameters.
+ type: dict
+ suboptions:
+ as_path:
+ description: AS-path attribute comparison.
+ type: str
+ choices: [confed, ignore, multipath-relax]
+ graceful_restart:
+ description: Enable graceful restart.
+ type: bool
+ log_neighbor_changes:
+ description: Log neighbor up/down changes.
+ type: bool
+ no_ipv4_unicast:
+ description: Disable IPv4 unicast default.
+ type: bool
+ neighbors:
+ description: BGP neighbors.
+ type: list
+ elements: dict
+ suboptions:
+ neighbor_address:
+ description: Neighbor IP address.
+ type: str
+ required: true
+ remote_as:
+ description: Neighbor AS number.
+ type: int
+ description:
+ description: Neighbor description.
+ type: str
+ disable_connected_check:
+ description: Disable connected route check.
+ type: bool
+ ebgp_multihop:
+ description: EBGP multihop TTL.
+ type: int
+ local_as:
+ description: Local AS number.
+ type: int
+ password:
+ description: MD5 password for neighbor.
+ type: str
+ peer_group:
+ description: Peer group name.
+ type: str
+ shutdown:
+ description: Shutdown neighbor.
+ type: bool
+ timers:
+ description: Neighbor timers.
+ type: dict
+ suboptions:
+ holdtime:
+ description: Hold time in seconds.
+ type: int
+ keepalive:
+ description: Keepalive interval in seconds.
+ type: int
+ update_source:
+ description: Source interface/IP for updates.
+ type: str
+ peer_groups:
+ description: BGP peer groups.
+ type: list
+ elements: dict
+ suboptions:
+ peer_group:
+ description: Peer group name.
+ type: str
+ required: true
+ remote_as:
+ description: Peer group AS number.
+ type: int
+ description:
+ description: Peer group description.
+ type: str
+ ebgp_multihop:
+ description: EBGP multihop TTL.
+ type: int
+ password:
+ description: MD5 password.
+ type: str
+ timers:
+ description: Peer group timers.
+ type: dict
+ suboptions:
+ holdtime:
+ description: Hold time in seconds.
+ type: int
+ keepalive:
+ description: Keepalive interval in seconds.
+ type: int
+ update_source:
+ description: Source interface/IP for updates.
+ type: str
+ state:
+ description:
+ - Desired state of the BGP global configuration.
+ - C(merged) adds or updates without removing existing config.
+ - C(replaced) replaces the entire BGP configuration.
+ - C(deleted) removes BGP 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).
+ - BGP system-as must be defined before any other BGP configuration.
+"""
+
+EXAMPLES = r"""
+- name: Merge BGP global configuration
+ vyos.rest.vyos_bgp_global:
+ config:
+ as_number: 65000
+ parameters:
+ router_id: 192.0.1.1
+ neighbors:
+ - neighbor_address: 192.0.2.1
+ remote_as: 65001
+ description: peer1
+ timers:
+ holdtime: 30
+ keepalive: 10
+ peer_groups:
+ - peer_group: PG1
+ remote_as: 65002
+ state: merged
+
+- name: Delete BGP configuration
+ vyos.rest.vyos_bgp_global:
+ state: deleted
+
+- name: Gather BGP global configuration
+ vyos.rest.vyos_bgp_global:
+ state: gathered
+"""
+
+RETURN = r"""
+before:
+ description: BGP configuration before this module ran.
+ returned: always
+ type: dict
+after:
+ description: BGP 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 BGP 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", "bgp"]
+
+
+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 "log-neighbor-changes" in raw:
+ result["log_neighbor_changes"] = True
+ if "no-ipv4-unicast" in raw:
+ result["no_ipv4_unicast"] = True
+ if "graceful-restart" in raw:
+ result["graceful_restart"] = True
+ bp = raw.get("bestpath", {}) or {}
+ if bp:
+ bestpath = {}
+ if "as-path" in bp:
+ bestpath["as_path"] = bp["as-path"]
+ if bestpath:
+ result["bestpath"] = bestpath
+ conf = raw.get("confederation", {}) or {}
+ if conf:
+ confederation = {}
+ if "identifier" in conf:
+ confederation["identifier"] = int(conf["identifier"])
+ if "peers" in conf:
+ peers = conf["peers"]
+ if isinstance(peers, list):
+ confederation["peers"] = [int(p) for p in peers]
+ else:
+ confederation["peers"] = [int(peers)]
+ if confederation:
+ result["confederation"] = confederation
+ return result
+
+
+def _parse_neighbor(nb_id, data):
+ nb = {"neighbor_address": nb_id}
+ data = data or {}
+ if "remote-as" in data:
+ nb["remote_as"] = int(data["remote-as"])
+ if "description" in data:
+ nb["description"] = data["description"]
+ if "ebgp-multihop" in data:
+ nb["ebgp_multihop"] = int(data["ebgp-multihop"])
+ if "local-as" in data:
+ nb["local_as"] = int(data["local-as"])
+ if "password" in data:
+ nb["password"] = data["password"]
+ if "peer-group" in data:
+ nb["peer_group"] = data["peer-group"]
+ if "shutdown" in data:
+ nb["shutdown"] = True
+ if "update-source" in data:
+ nb["update_source"] = data["update-source"]
+ if "disable-connected-check" in data:
+ nb["disable_connected_check"] = True
+ timers = data.get("timers", {}) or {}
+ if timers:
+ t = {}
+ if "holdtime" in timers:
+ t["holdtime"] = int(timers["holdtime"])
+ if "keepalive" in timers:
+ t["keepalive"] = int(timers["keepalive"])
+ if t:
+ nb["timers"] = t
+ return nb
+
+
+def _parse_peer_group(pg_name, data):
+ pg = {"peer_group": pg_name}
+ data = data or {}
+ if "remote-as" in data:
+ pg["remote_as"] = int(data["remote-as"])
+ if "description" in data:
+ pg["description"] = data["description"]
+ if "ebgp-multihop" in data:
+ pg["ebgp_multihop"] = int(data["ebgp-multihop"])
+ if "password" in data:
+ pg["password"] = data["password"]
+ if "update-source" in data:
+ pg["update_source"] = data["update-source"]
+ timers = data.get("timers", {}) or {}
+ if timers:
+ t = {}
+ if "holdtime" in timers:
+ t["holdtime"] = int(timers["holdtime"])
+ if "keepalive" in timers:
+ t["keepalive"] = int(timers["keepalive"])
+ if t:
+ pg["timers"] = t
+ return pg
+
+
+def get_running_config(vyos):
+ raw = vyos.get_config(_BASE)
+ if not raw or not isinstance(raw, dict):
+ return {}
+ result = {}
+
+ if "system-as" in raw:
+ result["as_number"] = int(raw["system-as"])
+
+ params = _parse_parameters(raw.get("parameters"))
+ if params:
+ result["parameters"] = params
+
+ neighbors = []
+ for nb_id, data in sorted((raw.get("neighbor") or {}).items()):
+ neighbors.append(_parse_neighbor(nb_id, data))
+ if neighbors:
+ result["neighbors"] = neighbors
+
+ peer_groups = []
+ for pg_name, data in sorted((raw.get("peer-group") or {}).items()):
+ peer_groups.append(_parse_peer_group(pg_name, data))
+ if peer_groups:
+ result["peer_groups"] = peer_groups
+
+ return result
+
+
+def _neighbor_cmds(nb, have_nb):
+ cmds = []
+ nb_addr = nb["neighbor_address"]
+ nbase = _BASE + ["neighbor", nb_addr]
+ have_nb = have_nb or {}
+
+ if nb.get("remote_as") and nb["remote_as"] != have_nb.get("remote_as"):
+ cmds.append(("set", nbase + ["remote-as", str(nb["remote_as"])]))
+ if nb.get("description") and nb["description"] != have_nb.get("description"):
+ cmds.append(("set", nbase + ["description", nb["description"]]))
+ if nb.get("ebgp_multihop") and nb["ebgp_multihop"] != have_nb.get("ebgp_multihop"):
+ cmds.append(("set", nbase + ["ebgp-multihop", str(nb["ebgp_multihop"])]))
+ if nb.get("local_as") and nb["local_as"] != have_nb.get("local_as"):
+ cmds.append(("set", nbase + ["local-as", str(nb["local_as"])]))
+ if nb.get("password") and nb["password"] != have_nb.get("password"):
+ cmds.append(("set", nbase + ["password", nb["password"]]))
+ if nb.get("peer_group") and nb["peer_group"] != have_nb.get("peer_group"):
+ cmds.append(("set", nbase + ["peer-group", nb["peer_group"]]))
+ if nb.get("update_source") and nb["update_source"] != have_nb.get("update_source"):
+ cmds.append(("set", nbase + ["update-source", nb["update_source"]]))
+ if nb.get("shutdown") and not have_nb.get("shutdown"):
+ cmds.append(("set", nbase + ["shutdown"]))
+ if nb.get("disable_connected_check") and not have_nb.get("disable_connected_check"):
+ cmds.append(("set", nbase + ["disable-connected-check"]))
+
+ want_t = nb.get("timers") or {}
+ have_t = have_nb.get("timers") or {}
+ if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"):
+ cmds.append(("set", nbase + ["timers", "holdtime", str(want_t["holdtime"])]))
+ if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"):
+ cmds.append(("set", nbase + ["timers", "keepalive", str(want_t["keepalive"])]))
+
+ return cmds
+
+
+def _peer_group_cmds(pg, have_pg):
+ cmds = []
+ pg_name = pg["peer_group"]
+ pbase = _BASE + ["peer-group", pg_name]
+ have_pg = have_pg or {}
+
+ if pg.get("remote_as") and pg["remote_as"] != have_pg.get("remote_as"):
+ cmds.append(("set", pbase + ["remote-as", str(pg["remote_as"])]))
+ if pg.get("description") and pg["description"] != have_pg.get("description"):
+ cmds.append(("set", pbase + ["description", pg["description"]]))
+ if pg.get("ebgp_multihop") and pg["ebgp_multihop"] != have_pg.get("ebgp_multihop"):
+ cmds.append(("set", pbase + ["ebgp-multihop", str(pg["ebgp_multihop"])]))
+ if pg.get("password") and pg["password"] != have_pg.get("password"):
+ cmds.append(("set", pbase + ["password", pg["password"]]))
+ if pg.get("update_source") and pg["update_source"] != have_pg.get("update_source"):
+ cmds.append(("set", pbase + ["update-source", pg["update_source"]]))
+
+ want_t = pg.get("timers") or {}
+ have_t = have_pg.get("timers") or {}
+ if want_t.get("holdtime") and want_t["holdtime"] != have_t.get("holdtime"):
+ cmds.append(("set", pbase + ["timers", "holdtime", str(want_t["holdtime"])]))
+ if want_t.get("keepalive") and want_t["keepalive"] != have_t.get("keepalive"):
+ cmds.append(("set", pbase + ["timers", "keepalive", str(want_t["keepalive"])]))
+
+ 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 {}
+
+ # system-as — must be first
+ if config.get("as_number") and config["as_number"] != have.get("as_number"):
+ cmds.append(("set", _BASE + ["system-as", str(config["as_number"])]))
+
+ # parameters
+ params = config.get("parameters") or {}
+ have_params = have.get("parameters") or {}
+ if params.get("router_id") and params["router_id"] != have_params.get("router_id"):
+ cmds.append(("set", _BASE + ["parameters", "router-id", params["router_id"]]))
+ if params.get("log_neighbor_changes") and not have_params.get("log_neighbor_changes"):
+ cmds.append(("set", _BASE + ["parameters", "log-neighbor-changes"]))
+ if params.get("no_ipv4_unicast") and not have_params.get("no_ipv4_unicast"):
+ cmds.append(("set", _BASE + ["parameters", "no-ipv4-unicast"]))
+ if params.get("graceful_restart") and not have_params.get("graceful_restart"):
+ cmds.append(("set", _BASE + ["parameters", "graceful-restart"]))
+ bp = params.get("bestpath") or {}
+ have_bp = have_params.get("bestpath") or {}
+ if bp.get("as_path") and bp["as_path"] != have_bp.get("as_path"):
+ cmds.append(("set", _BASE + ["parameters", "bestpath", "as-path", bp["as_path"]]))
+
+ # neighbors
+ have_nb_map = {n["neighbor_address"]: n for n in (have.get("neighbors") or [])}
+ for nb in config.get("neighbors") or []:
+ cmds += _neighbor_cmds(nb, have_nb_map.get(nb["neighbor_address"]))
+
+ # peer_groups
+ have_pg_map = {p["peer_group"]: p for p in (have.get("peer_groups") or [])}
+ for pg in config.get("peer_groups") or []:
+ cmds += _peer_group_cmds(pg, have_pg_map.get(pg["peer_group"]))
+
+ return cmds
+
+
+ARGUMENT_SPEC = dict(
+ config=dict(
+ type="dict",
+ options=dict(
+ as_number=dict(type="int", required=True),
+ parameters=dict(
+ type="dict",
+ options=dict(
+ router_id=dict(type="str"),
+ log_neighbor_changes=dict(type="bool"),
+ no_ipv4_unicast=dict(type="bool"),
+ graceful_restart=dict(type="bool"),
+ bestpath=dict(
+ type="dict",
+ options=dict(
+ as_path=dict(
+ type="str",
+ choices=["confed", "ignore", "multipath-relax"],
+ ),
+ ),
+ ),
+ confederation=dict(
+ type="dict",
+ options=dict(
+ identifier=dict(type="int"),
+ peers=dict(type="list", elements="int"),
+ ),
+ ),
+ ),
+ ),
+ neighbors=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ neighbor_address=dict(type="str", required=True),
+ remote_as=dict(type="int"),
+ description=dict(type="str"),
+ disable_connected_check=dict(type="bool"),
+ ebgp_multihop=dict(type="int"),
+ local_as=dict(type="int"),
+ password=dict(type="str", no_log=True),
+ peer_group=dict(type="str"),
+ shutdown=dict(type="bool"),
+ timers=dict(
+ type="dict",
+ options=dict(
+ holdtime=dict(type="int"),
+ keepalive=dict(type="int"),
+ ),
+ ),
+ update_source=dict(type="str"),
+ ),
+ ),
+ peer_groups=dict(
+ type="list",
+ elements="dict",
+ options=dict(
+ peer_group=dict(type="str", required=True),
+ remote_as=dict(type="int"),
+ description=dict(type="str"),
+ ebgp_multihop=dict(type="int"),
+ password=dict(type="str", no_log=True),
+ timers=dict(
+ type="dict",
+ options=dict(
+ holdtime=dict(type="int"),
+ keepalive=dict(type="int"),
+ ),
+ ),
+ update_source=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()