diff options
Diffstat (limited to 'plugins/modules/vyos_ospfv2.py')
| -rw-r--r-- | plugins/modules/vyos_ospfv2.py | 955 |
1 files changed, 955 insertions, 0 deletions
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() |
