diff options
| author | omnom62 <omnom62@outlook.com> | 2026-05-19 08:44:50 +1000 |
|---|---|---|
| committer | omnom62 <omnom62@outlook.com> | 2026-05-19 08:44:50 +1000 |
| commit | 60866aeb2f743a404e045ce64a2c0912c689c6a9 (patch) | |
| tree | 2bbb68c0def8e9136ebf26d4ace2c527d63a7c99 /plugins/modules/vyos_prefix_lists.py | |
| parent | 92b7323c353f473c9d5bdb7930e9490a5dafcd41 (diff) | |
| download | rest.vyos-60866aeb2f743a404e045ce64a2c0912c689c6a9.tar.gz rest.vyos-60866aeb2f743a404e045ce64a2c0912c689c6a9.zip | |
route_map
Diffstat (limited to 'plugins/modules/vyos_prefix_lists.py')
| -rw-r--r-- | plugins/modules/vyos_prefix_lists.py | 483 |
1 files changed, 314 insertions, 169 deletions
diff --git a/plugins/modules/vyos_prefix_lists.py b/plugins/modules/vyos_prefix_lists.py index 63fa584..5334e7a 100644 --- a/plugins/modules/vyos_prefix_lists.py +++ b/plugins/modules/vyos_prefix_lists.py @@ -1,6 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# GNU General Public License v3.0+ from __future__ import absolute_import, division, print_function @@ -10,20 +9,22 @@ __metaclass__ = type DOCUMENTATION = r""" --- module: vyos_prefix_lists -short_description: Prefix-Lists resource module via REST API. +short_description: Manage prefix-list configuration on VyOS devices using REST API description: - - Manages IPv4 and IPv6 prefix lists on VyOS via the HTTPS REST API. + - Manages IPv4 and IPv6 prefix lists on VyOS 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: List of prefix-list configurations. + description: List of prefix-list configurations grouped by address family. type: list elements: dict suboptions: afi: - description: Address family. + description: Address family identifier. type: str choices: [ipv4, ipv6] required: true @@ -37,7 +38,7 @@ options: type: str required: true description: - description: Description. + description: Prefix list description. type: str entries: description: Prefix list rules. @@ -49,9 +50,10 @@ options: type: int required: true description: + description: Rule description. type: str action: - description: permit or deny. + description: Permit or deny. type: str choices: [permit, deny] ge: @@ -63,204 +65,347 @@ options: prefix: description: Network prefix to match. type: str + state: + description: + - Desired state of the prefix-list configuration. + - C(merged) adds or updates entries without removing existing ones. + - C(replaced) replaces each named prefix list mentioned in config. + - C(overridden) replaces all prefix lists for the given AFIs. + - C(deleted) removes prefix lists. Without config removes all. + - C(gathered) returns current configuration as structured data. type: str choices: [merged, replaced, overridden, deleted, gathered] default: merged - hostname: - type: str - required: true - port: - type: int - default: 443 - api_key: - type: str - required: true - no_log: true - timeout: - type: int - default: 30 - verify_ssl: - type: bool - default: false + +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 prefix list configuration + vyos.rest.vyos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AnsibleIPv4PrefixList + description: PL configured by ansible + entries: + - sequence: 2 + action: permit + prefix: 92.168.10.0/26 + le: 32 + - sequence: 3 + action: deny + prefix: 72.168.2.0/24 + ge: 26 + - afi: ipv6 + prefix_lists: + - name: AllowIPv6Prefix + entries: + - sequence: 5 + action: permit + prefix: 2001:db8:8000::/35 + le: 37 + state: merged + +- name: Delete all prefix lists + vyos.rest.vyos_prefix_lists: + state: deleted + +- name: Gather current prefix list configuration + vyos.rest.vyos_prefix_lists: + state: gathered """ RETURN = r""" before: + description: Prefix list configuration before this module ran. returned: always type: list + after: + description: Prefix list 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 prefix list 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_rest import ( - VYOS_REST_CONNECTION_ARGSPEC, - VyOSRestClient, - VyOSRestError, -) +from ansible_collections.vyos.rest.plugins.module_utils.vyos import VyOSModule +# AFI → API key mapping _PL_KEY = {"ipv4": "prefix-list", "ipv6": "prefix-list6"} -def _get(client): - try: - r = client.retrieve_show_config(["policy"]) - data = r.get("data") or {} - out = [] - for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]: - pl_data = data.get(key) or {} - if not isinstance(pl_data, dict): - continue - pls = [] - for pl_name, pl_info in pl_data.items(): - entry = {"name": pl_name} - if isinstance(pl_info, dict): - if "description" in pl_info: - entry["description"] = pl_info["description"] - rules = [] - for seq, rdata in (pl_info.get("rule") or {}).items(): - if isinstance(rdata, dict): - rules.append({"sequence": int(seq), **rdata}) - if rules: - entry["entries"] = rules - pls.append(entry) - if pls: - out.append({"afi": afi, "prefix_lists": pls}) - return out - except VyOSRestError: +# ------------------------------------------------------------ +# Parsing: API response → argspec list +# ------------------------------------------------------------ + + +def get_running_config(vyos): + """ + Fetch current prefix list config and return in argspec list format: + [{"afi": "ipv4", "prefix_lists": [...]}, {"afi": "ipv6", ...}] + """ + raw = vyos.get_config(["policy"]) + if not raw or not isinstance(raw, dict): return [] + result = [] + for afi, api_key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]: + pl_data = raw.get(api_key) or {} + if not isinstance(pl_data, dict): + continue + + pls = [] + for pl_name, pl_info in sorted(pl_data.items()): + entry = {"name": pl_name} + pl_info = pl_info or {} + + if pl_info.get("description"): + entry["description"] = pl_info["description"] + + rules = [] + for seq, rdata in sorted( + (pl_info.get("rule") or {}).items(), + key=lambda x: int(x[0]), + ): + rdata = rdata or {} + rule = {"sequence": int(seq)} + if rdata.get("action"): + rule["action"] = rdata["action"] + if rdata.get("prefix"): + rule["prefix"] = rdata["prefix"] + if rdata.get("ge") is not None: + rule["ge"] = int(rdata["ge"]) + if rdata.get("le") is not None: + rule["le"] = int(rdata["le"]) + if rdata.get("description"): + rule["description"] = rdata["description"] + rules.append(rule) + + if rules: + entry["entries"] = rules + pls.append(entry) + + if pls: + result.append({"afi": afi, "prefix_lists": pls}) + + return result + + +# ------------------------------------------------------------ +# Internal normalization: argspec list → nested dicts for diffing +# ------------------------------------------------------------ + + +def _normalize(config): + """ + Convert argspec list to nested dict keyed by afi → pl_name → rule_seq. + { + "ipv4": { + "AnsibleIPv4PrefixList": { + "description": "...", + "rules": {2: {...}, 3: {...}} + } + }, + "ipv6": {...} + } + """ + result = {"ipv4": {}, "ipv6": {}} + for entry in config or []: + afi = entry.get("afi") + if afi not in result: + continue + for pl in entry.get("prefix_lists") or []: + name = pl["name"] + rules = {} + for r in pl.get("entries") or []: + seq = r["sequence"] + rules[seq] = {k: v for k, v in r.items() if k != "sequence" and v is not None} + result[afi][name] = { + "description": pl.get("description"), + "rules": rules, + } + return result + + +# ------------------------------------------------------------ +# Command builders +# ------------------------------------------------------------ + + +def _rule_cmds(base, seq, want_rule, have_rule, state): + """Build commands for a single rule entry.""" + cmds = [] + rbase = base + ["rule", str(seq)] + + if seq not in (have_rule or {}): + # New rule + for field, api_key in [ + ("action", "action"), + ("prefix", "prefix"), + ("description", "description"), + ]: + if want_rule.get(field): + cmds.append(("set", rbase + [api_key, want_rule[field]])) + if want_rule.get("ge") is not None: + cmds.append(("set", rbase + ["ge", str(want_rule["ge"])])) + if want_rule.get("le") is not None: + cmds.append(("set", rbase + ["le", str(want_rule["le"])])) + else: + # Existing rule — update changed fields + h = have_rule[seq] + for field, api_key in [ + ("action", "action"), + ("prefix", "prefix"), + ("description", "description"), + ]: + if want_rule.get(field) and want_rule[field] != h.get(field): + cmds.append(("set", rbase + [api_key, want_rule[field]])) + for field in ("ge", "le"): + if want_rule.get(field) is not None and want_rule[field] != h.get(field): + cmds.append(("set", rbase + [field, str(want_rule[field])])) -def _apply(client, entry, commands): - afi = entry["afi"] - key = _PL_KEY[afi] - for pl in entry.get("prefix_lists") or []: - base = ["policy", key, pl["name"]] - client.configure_set(base) - commands.append("set policy {k} {n}".format(k=key, n=pl["name"])) - if pl.get("description"): - client.configure_set(base + ["description"], pl["description"]) - for rule in pl.get("entries") or []: - rb = base + ["rule", str(rule["sequence"])] - client.configure_set(rb) - commands.append( - "set policy {k} {n} rule {s}".format( - k=key, - n=pl["name"], - s=rule["sequence"], - ), - ) - if rule.get("action"): - client.configure_set(rb + ["action"], rule["action"]) - commands.append( - "set policy {k} {n} rule {s} action {a}".format( - k=key, - n=pl["name"], - s=rule["sequence"], - a=rule["action"], - ), - ) - if rule.get("prefix"): - client.configure_set(rb + ["prefix"], rule["prefix"]) - if rule.get("ge") is not None: - client.configure_set(rb + ["ge"], str(rule["ge"])) - if rule.get("le") is not None: - client.configure_set(rb + ["le"], str(rule["le"])) - if rule.get("description"): - client.configure_set(rb + ["description"], rule["description"]) + return cmds + + +def build_commands(config, have_raw, state): + """ + Build command tuples to move from have → want. + """ + cmds = [] + + if state == "deleted": + if not config: + # Delete all prefix lists for both AFIs + for afi, api_key in _PL_KEY.items(): + # Only delete if something exists + if any(e.get("afi") == afi for e in have_raw): + cmds.append(("delete", ["policy", api_key])) + else: + # Delete only specified prefix lists + for entry in config: + afi = entry["afi"] + api_key = _PL_KEY[afi] + for pl in entry.get("prefix_lists") or []: + cmds.append(("delete", ["policy", api_key, pl["name"]])) + return cmds + + want = _normalize(config) + have = _normalize(have_raw) + + for afi, api_key in _PL_KEY.items(): + want_afi = want.get(afi, {}) + have_afi = have.get(afi, {}) + + if state == "overridden": + # Delete PLs present on device but not in want + for pl_name in set(have_afi) - set(want_afi): + cmds.append(("delete", ["policy", api_key, pl_name])) + + for pl_name, want_pl in want_afi.items(): + have_pl = have_afi.get(pl_name, {}) + base = ["policy", api_key, pl_name] + + if state == "replaced" and pl_name in have_afi: + # Full replacement — delete existing then re-set + cmds.append(("delete", ["policy", api_key, pl_name])) + have_pl = {} + + # Description + if want_pl.get("description") and want_pl["description"] != have_pl.get("description"): + cmds.append(("set", base + ["description", want_pl["description"]])) + + # Rules + want_rules = want_pl.get("rules", {}) + have_rules = have_pl.get("rules", {}) + + if state == "replaced": + # have_rules is now empty after delete above + have_rules = {} + + for seq, want_rule in want_rules.items(): + cmds += _rule_cmds(base, seq, want_rule, have_rules, state) + + # merged: leave extra have rules alone + # replaced: they were deleted with the parent node above + + return cmds + + +# ------------------------------------------------------------ +# Argument spec +# ------------------------------------------------------------ + +ARGUMENT_SPEC = dict( + config=dict(type="list", elements="dict"), + state=dict( + default="merged", + choices=["merged", "replaced", "overridden", "deleted", "gathered"], + ), +) + + +# ------------------------------------------------------------ +# Main +# ------------------------------------------------------------ def main(): - argument_spec = dict( - config=dict( - type="list", - elements="dict", - options=dict( - afi=dict(type="str", required=True, choices=["ipv4", "ipv6"]), - prefix_lists=dict( - type="list", - elements="dict", - options=dict( - name=dict(type="str", required=True), - description=dict(type="str"), - entries=dict( - type="list", - elements="dict", - options=dict( - sequence=dict(type="int", required=True), - description=dict(type="str"), - action=dict(type="str", choices=["permit", "deny"]), - ge=dict(type="int"), - le=dict(type="int"), - prefix=dict(type="str"), - ), - ), - ), - ), - ), - ), - state=dict( - type="str", - default="merged", - choices=["merged", "replaced", "overridden", "deleted", "gathered"], - ), - ) - argument_spec.update(VYOS_REST_CONNECTION_ARGSPEC) - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - client = VyOSRestClient(module) + + module = AnsibleModule(ARGUMENT_SPEC, supports_check_mode=True) + + vyos = VyOSModule(module) + state = module.params["state"] config = module.params.get("config") or [] - commands = [] - changed = False - before = _get(client) + + have = get_running_config(vyos) if state == "gathered": - module.exit_json(changed=False, gathered=before, before=before, commands=[]) + module.exit_json(changed=False, gathered=have) + + commands = build_commands(config, have, state) + if module.check_mode: - module.exit_json(changed=True, before=before, commands=["(check mode)"]) - - try: - if state == "deleted" and not config: - for afi, key in [("ipv4", "prefix-list"), ("ipv6", "prefix-list6")]: - try: - client.configure_delete(["policy", key]) - commands.append("delete policy {k}".format(k=key)) - except VyOSRestError: - pass - changed = True - elif state == "deleted" and config: - for entry in config: - key = _PL_KEY[entry["afi"]] - for pl in entry.get("prefix_lists") or []: - try: - client.configure_delete(["policy", key, pl["name"]]) - commands.append("delete policy {k} {n}".format(k=key, n=pl["name"])) - changed = True - except VyOSRestError: - pass - elif state in ("merged", "replaced", "overridden"): - if state in ("replaced", "overridden"): - for entry in config: - key = _PL_KEY[entry["afi"]] - for pl in entry.get("prefix_lists") or []: - try: - client.configure_delete(["policy", key, pl["name"]]) - except VyOSRestError: - pass - for entry in config: - _apply(client, entry, commands) - changed = True - except VyOSRestError as exc: - module.fail_json(msg=str(exc)) + 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, + ) - after = _get(client) if changed else before - module.exit_json(changed=changed, before=before, after=after, commands=commands) + module.exit_json(changed=False, before=have, after=have, commands=[]) if __name__ == "__main__": |
