diff options
| author | omnom62 <omnom62@outlook.com> | 2026-07-01 12:56:25 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-07-01 12:56:25 +1000 |
| commit | aad7d2468a25650b7b6aa21ea9cf0757bd6ef6de (patch) | |
| tree | 68a03096006256249e08380c8fa94c1f4c3f2e0a /plugins/modules | |
| parent | a2c9a7b8cd03e34f2b50d925901b8a56a898a182 (diff) | |
| download | rest.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.py | 572 |
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() |
