#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright 2024 Red Hat # 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_snmp_server short_description: Manage SNMP server configuration on VyOS devices using REST API description: - Manages SNMP server configuration on VyOS devices via the REST API. - Supports communities, listen addresses, contact/location/description scalar fields, trap target, and SNMPv3 (engine ID, groups, users, views). - Uses REST API (C(connection=httpapi)) instead of CLI. version_added: "1.0.0" author: your_name (@yourhandle) options: config: description: SNMP server configuration. type: dict suboptions: communities: description: SNMP community configuration. type: list elements: dict suboptions: name: description: Community name. type: str required: true clients: description: IP addresses of SNMP clients allowed to contact the system. type: list elements: str networks: description: Subnets of SNMP clients allowed to contact the system. type: list elements: str authorization_type: description: Authorization type. type: str choices: ['ro', 'rw'] contact: description: Contact person for the system. type: str description: description: System description. type: str location: description: System location. type: str smux_peer: description: Register a subtree for SMUX-based processing. type: str trap_source: description: SNMP trap source address. type: str listen_addresses: description: IP addresses to listen for incoming SNMP requests. type: list elements: dict suboptions: address: description: IP address. type: str required: true port: description: UDP port (default 161). type: int trap_target: description: SNMP trap target. type: dict suboptions: address: description: IP address of trap target. type: str community: description: Community string for traps. type: str port: description: Destination port for trap notifications. type: int snmp_v3: description: SNMPv3 configuration. type: dict suboptions: engine_id: description: EngineID as a hex string. type: str groups: description: SNMPv3 groups. type: list elements: dict suboptions: group: description: Group name. type: str required: true mode: description: Access mode. type: str choices: ['ro', 'rw'] seclevel: description: Security level. type: str choices: ['auth', 'priv'] view: description: View name for this group. type: str users: description: SNMPv3 users. type: list elements: dict suboptions: user: description: Username. type: str required: true authentication: description: Authentication configuration. type: dict suboptions: type: description: Authentication protocol. type: str choices: ['md5', 'sha'] encrypted_key: description: Encrypted authentication password. type: str plaintext_key: description: Plaintext authentication password (will be encrypted on device). type: str no_log: true privacy: description: Privacy configuration. type: dict suboptions: type: description: Privacy protocol. type: str choices: ['des', 'aes'] encrypted_key: description: Encrypted privacy password. type: str plaintext_key: description: Plaintext privacy password (will be encrypted on device). type: str no_log: true group: description: Group membership for this user. type: str mode: description: Access mode. type: str choices: ['ro', 'rw'] tsm_key: description: TSM certificate fingerprint or filename. type: str trap_targets: description: SNMPv3 trap/inform targets. type: list elements: dict suboptions: address: description: IP/IPv6 address of trap target. type: str port: description: TCP/UDP port for traps/informs. type: int protocol: description: Notification protocol. type: str choices: ['tcp', 'udp'] type: description: Notification type. type: str choices: ['inform', 'trap'] authentication: description: Authentication for this trap target. type: dict suboptions: type: description: Authentication protocol. type: str choices: ['md5', 'sha'] encrypted_key: description: Encrypted authentication password. type: str plaintext_key: description: Plaintext authentication password (will be encrypted on device). type: str no_log: true privacy: description: Privacy for this trap target. type: dict suboptions: type: description: Privacy protocol. type: str choices: ['des', 'aes'] encrypted_key: description: Encrypted privacy password. type: str plaintext_key: description: Plaintext privacy password (will be encrypted on device). type: str no_log: true views: description: SNMPv3 views. type: list elements: dict suboptions: view: description: View name. type: str required: true oid: description: OID for this view. type: str exclude: description: Excluded OID subtree. type: str mask: description: Bit-mask for OID subidentifiers. type: str state: description: - Desired state of SNMP configuration. - C(merged) adds or updates provided config without removing existing entries. - C(replaced) and C(overridden) perform a full replacement — entries not in the task config are removed. - C(deleted) removes all SNMP configuration with a single delete command. - C(gathered) returns current device config as structured data, no changes made. type: str default: merged choices: - merged - replaced - overridden - deleted - gathered notes: - Tested against VyOS 1.5q2. """ EXAMPLES = r""" - name: Merge SNMP configuration vyos.rest.vyos_snmp_server: config: communities: - name: switches authorization_type: rw - name: bridges clients: - 1.1.1.1 - 12.1.1.10 contact: admin2@ex.com listen_addresses: - address: 20.1.1.1 - address: 100.1.2.1 port: 33 snmp_v3: users: - user: admin_user authentication: plaintext_key: abc1234567 type: sha privacy: plaintext_key: abc1234567 type: aes state: merged # ------------------------------------------------------------------------ - name: Replace SNMP configuration vyos.rest.vyos_snmp_server: config: communities: - name: bridges networks: - 1.1.1.0/24 - 12.1.1.0/24 location: "RDU, NC" listen_addresses: - address: 100.1.2.1 port: 33 snmp_v3: groups: - group: default view: default users: - user: admin_user authentication: encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633 type: sha privacy: encrypted_key: 33f8bfd6b69ee03a184818a4daea503c9e579633 type: aes group: default views: - view: default oid: "1" state: replaced # ------------------------------------------------------------------------ - name: Delete all SNMP configuration vyos.rest.vyos_snmp_server: state: deleted # ------------------------------------------------------------------------ - name: Gather current SNMP configuration vyos.rest.vyos_snmp_server: state: gathered """ RETURN = r""" before: description: SNMP configuration before this module ran. returned: when I(state) is C(merged), C(replaced), C(overridden) or C(deleted) type: dict after: description: SNMP configuration after this module ran. returned: when changed type: dict commands: description: List of API command dicts sent to the device. returned: always type: list gathered: description: Current SNMP configuration as structured data. returned: when I(state) is C(gathered) type: dict response: description: Raw API response. returned: when changes are applied type: dict saved: description: Result of save_config after applying changes. 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 # ------------------------------------------------------------ # Constants # ------------------------------------------------------------ SNMP_BASE = ["service", "snmp"] # Scalar fields that map directly: argspec_key → api_key SCALAR_FIELDS = { "contact": "contact", "description": "description", "location": "location", "smux_peer": "smux-peer", "trap_source": "trap-source", } # ------------------------------------------------------------ # Generic helpers # ------------------------------------------------------------ def to_list(value): """Coerce None / str / list → list.""" if value is None: return [] if isinstance(value, list): return value if isinstance(value, str): return [value] if isinstance(value, dict): return list(value.keys()) return [str(value)] # ------------------------------------------------------------ # Parsing: API response → Ansible argspec dict # ------------------------------------------------------------ def _parse_communities(raw): """ API: {"bridges": {"client": [...], "network": [...]}, "switches": {"authorization": "rw"}} → [{"name": "bridges", "clients": [...], ...}, ...] """ if not raw or not isinstance(raw, dict): return [] result = [] for name, data in sorted(raw.items()): entry = {"name": name} if not isinstance(data, dict): result.append(entry) continue if "authorization" in data: entry["authorization_type"] = data["authorization"] if "client" in data: entry["clients"] = sorted(to_list(data["client"])) if "network" in data: entry["networks"] = sorted(to_list(data["network"])) result.append(entry) return result def _parse_listen_addresses(raw): """ API: {"198.51.100.10": {"port": "33"}, "203.0.113.65": {}} → [{"address": "198.51.100.10", "port": 33}, {"address": "203.0.113.65"}] """ if not raw or not isinstance(raw, dict): return [] result = [] for addr, data in sorted(raw.items()): entry = {"address": addr} if isinstance(data, dict) and "port" in data: entry["port"] = int(data["port"]) result.append(entry) return result def _parse_trap_target(raw): """ API: {"address": "x.x.x.x"} or plain string address, optional port/community. """ if not raw: return None if isinstance(raw, str): return {"address": raw} entry = {} if "address" in raw: entry["address"] = raw["address"] if "community" in raw: entry["community"] = raw["community"] if "port" in raw: entry["port"] = int(raw["port"]) return entry if entry else None def _parse_v3_auth_privacy(raw, key): """ Parse auth or privacy block. API key is 'auth', argspec key is 'authentication'. API key is 'privacy', argspec key is 'privacy'. Returns dict with type and encrypted_key (never plaintext — device never returns it). """ block = raw.get(key) if isinstance(raw, dict) else None if not block: return None result = {} if "type" in block: result["type"] = block["type"] # API uses 'encrypted-password'; map to argspec 'encrypted_key' if "encrypted-password" in block: result["encrypted_key"] = block["encrypted-password"] if "plaintext-key" in block: result["plaintext_key"] = block["plaintext-key"] return result if result else None def _parse_v3_users(raw): """ API: {"adminuser": {"auth": {...}, "group": "testgroup", "privacy": {...}}} → [{"user": "adminuser", "authentication": {...}, "group": "testgroup", "privacy": {...}}] """ if not raw or not isinstance(raw, dict): return [] result = [] for username, data in sorted(raw.items()): entry = {"user": username} auth = _parse_v3_auth_privacy(data, "auth") if auth: entry["authentication"] = auth priv = _parse_v3_auth_privacy(data, "privacy") if priv: entry["privacy"] = priv if isinstance(data, dict): if "group" in data: entry["group"] = data["group"] if "mode" in data: entry["mode"] = data["mode"] if "tsm-key" in data: entry["tsm_key"] = data["tsm-key"] result.append(entry) return result def _parse_v3_groups(raw): """ API: {"testgroup": {"mode": "ro", "view": "default"}} → [{"group": "testgroup", "mode": "ro", "view": "default"}] """ if not raw or not isinstance(raw, dict): return [] result = [] for name, data in sorted(raw.items()): entry = {"group": name} if isinstance(data, dict): for key in ("mode", "seclevel", "view"): if key in data: entry[key] = data[key] result.append(entry) return result def _parse_v3_views(raw): """ API: {"default": {"oid": {"1": {}}}} → [{"view": "default", "oid": "1"}] OID is stored as a dict key with an empty dict value. """ if not raw or not isinstance(raw, dict): return [] result = [] for name, data in sorted(raw.items()): entry = {"view": name} if isinstance(data, dict) and "oid" in data: oid_data = data["oid"] if isinstance(oid_data, dict) and oid_data: # Extract the first (and usually only) OID key entry["oid"] = str(list(oid_data.keys())[0]) elif isinstance(oid_data, str): entry["oid"] = oid_data if isinstance(data, dict): if "exclude" in data: entry["exclude"] = data["exclude"] if "mask" in data: entry["mask"] = data["mask"] result.append(entry) return result def _parse_v3_trap_targets(raw): """ API: {"x.x.x.x": {"auth": {...}, "privacy": {...}, "port": "162", ...}} → [{"address": "x.x.x.x", ...}] """ if not raw or not isinstance(raw, dict): return [] result = [] for addr, data in sorted(raw.items()): entry = {"address": addr} if isinstance(data, dict): if "port" in data: entry["port"] = int(data["port"]) if "protocol" in data: entry["protocol"] = data["protocol"] if "type" in data: entry["type"] = data["type"] auth = _parse_v3_auth_privacy(data, "auth") if auth: entry["authentication"] = auth priv = _parse_v3_auth_privacy(data, "privacy") if priv: entry["privacy"] = priv result.append(entry) return result def parse_snmp_config(raw): """ Convert full API SNMP response dict → Ansible argspec dict. """ if not raw or not isinstance(raw, dict): return {} result = {} # Scalar fields for argspec_key, api_key in SCALAR_FIELDS.items(): if api_key in raw: result[argspec_key] = raw[api_key] # Communities communities = _parse_communities(raw.get("community")) if communities: result["communities"] = communities # Listen addresses listen = _parse_listen_addresses(raw.get("listen-address")) if listen: result["listen_addresses"] = listen # Trap target trap = _parse_trap_target(raw.get("trap-target")) if trap: result["trap_target"] = trap # SNMPv3 v3_raw = raw.get("v3") if v3_raw and isinstance(v3_raw, dict): v3 = {} if "engineid" in v3_raw: v3["engine_id"] = v3_raw["engineid"] groups = _parse_v3_groups(v3_raw.get("group")) if groups: v3["groups"] = groups users = _parse_v3_users(v3_raw.get("user")) if users: v3["users"] = users views = _parse_v3_views(v3_raw.get("view")) if views: v3["views"] = views trap_targets = _parse_v3_trap_targets(v3_raw.get("trap-target")) if trap_targets: v3["trap_targets"] = trap_targets if v3: result["snmp_v3"] = v3 return result def get_running_config(vyos): """Fetch and parse current SNMP config from device.""" try: raw = vyos.get_config(SNMP_BASE) except Exception as e: if "Configuration under specified path is empty" in str(e): return {} raise return parse_snmp_config(raw) # ------------------------------------------------------------ # Command builders: Ansible argspec dict → API command list # ------------------------------------------------------------ def _cmd(op, path): return {"op": op, "path": path} def _set(path): return _cmd("set", path) def _delete(path): return _cmd("delete", path) def _build_scalar_commands(want, have, state): """Build commands for simple string scalar fields.""" cmds = [] for argspec_key, api_key in SCALAR_FIELDS.items(): 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)) return cmds def _build_community_commands(want_list, have_list, state): """Build commands for SNMP communities.""" cmds = [] want_map = {c["name"]: c for c in (want_list or [])} have_map = {c["name"]: c for c in (have_list or [])} if state in ("replaced", "overridden"): # Delete communities not in want for name in have_map: if name not in want_map: cmds.append(_delete(SNMP_BASE + ["community", name])) for name, want_comm in want_map.items(): have_comm = have_map.get(name, {}) base = SNMP_BASE + ["community", name] if state in ("merged", "replaced", "overridden"): # authorization_type 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"])) # clients want_clients = set(want_comm.get("clients") or []) have_clients = set(have_comm.get("clients") or []) for c in want_clients - have_clients: cmds.append(_set(base + ["client", c])) if state in ("replaced", "overridden"): for c in have_clients - want_clients: cmds.append(_delete(base + ["client", c])) # networks want_nets = set(want_comm.get("networks") or []) have_nets = set(have_comm.get("networks") or []) for n in want_nets - have_nets: cmds.append(_set(base + ["network", n])) if state in ("replaced", "overridden"): for n in have_nets - want_nets: cmds.append(_delete(base + ["network", n])) return cmds def _build_listen_address_commands(want_list, have_list, state): """Build commands for listen-address entries.""" cmds = [] want_map = {e["address"]: e for e in (want_list or [])} have_map = {e["address"]: e for e in (have_list or [])} base = SNMP_BASE + ["listen-address"] if state in ("replaced", "overridden"): for addr in have_map: if addr not in want_map: cmds.append(_delete(base + [addr])) for addr, want_entry in want_map.items(): have_entry = have_map.get(addr, {}) want_port = want_entry.get("port") have_port = have_entry.get("port") if addr not in have_map: # New address if want_port: cmds.append(_set(base + [addr, "port", str(want_port)])) else: cmds.append(_set(base + [addr])) elif want_port != have_port: # Address exists but port changed — delete and re-set cmds.append(_delete(base + [addr])) if want_port: cmds.append(_set(base + [addr, "port", str(want_port)])) else: cmds.append(_set(base + [addr])) return cmds def _build_trap_target_commands(want, have, state): """Build commands for the v2 trap-target scalar dict.""" cmds = [] base = SNMP_BASE + ["trap-target"] if state in ("merged", "replaced", "overridden"): if want: want_addr = want.get("address") have_addr = have.get("address") if have else None if want_addr and want_addr != have_addr: cmds.append(_set(base + [want_addr])) if want.get("community"): cmds.append(_set(base + [want_addr, "community", want["community"]])) if want.get("port"): cmds.append(_set(base + [want_addr, "port", str(want["port"])])) if state in ("replaced", "overridden"): if have and (not want or have.get("address") != (want or {}).get("address")): cmds.append(_delete(base)) return cmds def _build_v3_auth_privacy_commands(base, want_block, have_block, api_key): """ Build set commands for a v3 auth or privacy block. api_key: 'auth' or 'privacy' """ cmds = [] if not want_block: return cmds block_base = base + [api_key] have_block = have_block or {} if want_block.get("type") and want_block["type"] != have_block.get("type"): cmds.append(_set(block_base + ["type", want_block["type"]])) if want_block.get("encrypted_key") and want_block["encrypted_key"] != have_block.get( "encrypted_key", ): cmds.append(_set(block_base + ["encrypted-password", want_block["encrypted_key"]])) if want_block.get("plaintext_key"): # Always set plaintext — we cannot compare it to the stored encrypted value cmds.append(_set(block_base + ["plaintext-key", want_block["plaintext_key"]])) return cmds def _build_v3_user_commands(want_list, have_list, state): """Build commands for SNMPv3 users.""" cmds = [] want_map = {u["user"]: u for u in (want_list or [])} have_map = {u["user"]: u for u in (have_list or [])} base = SNMP_BASE + ["v3", "user"] if state in ("replaced", "overridden"): for username in have_map: if username not in want_map: cmds.append(_delete(base + [username])) for username, want_user in want_map.items(): have_user = have_map.get(username, {}) user_base = base + [username] cmds += _build_v3_auth_privacy_commands( user_base, want_user.get("authentication"), have_user.get("authentication"), "auth", ) cmds += _build_v3_auth_privacy_commands( user_base, want_user.get("privacy"), have_user.get("privacy"), "privacy", ) if want_user.get("group") and want_user["group"] != have_user.get("group"): cmds.append(_set(user_base + ["group", want_user["group"]])) if want_user.get("mode") and want_user["mode"] != have_user.get("mode"): cmds.append(_set(user_base + ["mode", want_user["mode"]])) if want_user.get("tsm_key") and want_user["tsm_key"] != have_user.get("tsm_key"): cmds.append(_set(user_base + ["tsm-key", want_user["tsm_key"]])) return cmds def _build_v3_group_commands(want_list, have_list, state): """Build commands for SNMPv3 groups.""" cmds = [] want_map = {g["group"]: g for g in (want_list or [])} have_map = {g["group"]: g for g in (have_list or [])} base = SNMP_BASE + ["v3", "group"] if state in ("replaced", "overridden"): for name in have_map: if name not in want_map: cmds.append(_delete(base + [name])) for name, want_group in want_map.items(): have_group = have_map.get(name, {}) group_base = base + [name] for key, api_key in [("mode", "mode"), ("seclevel", "seclevel"), ("view", "view")]: want_val = want_group.get(key) have_val = have_group.get(key) if want_val and want_val != have_val: cmds.append(_set(group_base + [api_key, want_val])) if state in ("replaced", "overridden") and have_val and want_val != have_val: cmds.append(_delete(group_base + [api_key])) return cmds def _build_v3_view_commands(want_list, have_list, state): """Build commands for SNMPv3 views.""" cmds = [] want_map = {v["view"]: v for v in (want_list or [])} have_map = {v["view"]: v for v in (have_list or [])} base = SNMP_BASE + ["v3", "view"] if state in ("replaced", "overridden"): for name in have_map: if name not in want_map: cmds.append(_delete(base + [name])) for name, want_view in want_map.items(): have_view = have_map.get(name, {}) view_base = base + [name] want_oid = str(want_view["oid"]) if want_view.get("oid") else None have_oid = str(have_view.get("oid")) if have_view.get("oid") else None if want_oid and want_oid != have_oid: cmds.append(_set(view_base + ["oid", want_oid])) if state in ("replaced", "overridden") and have_oid and want_oid != have_oid: cmds.append(_delete(view_base + ["oid", have_oid])) for key in ("exclude", "mask"): want_val = want_view.get(key) have_val = have_view.get(key) if want_val and want_val != have_val: cmds.append(_set(view_base + [key, want_val])) return cmds def _build_v3_commands(want_v3, have_v3, state): """Build all SNMPv3 commands.""" cmds = [] want_v3 = want_v3 or {} have_v3 = have_v3 or {} # engine_id (maps to API key 'engineid') want_eid = want_v3.get("engine_id") have_eid = have_v3.get("engine_id") if want_eid and want_eid != have_eid: cmds.append(_set(SNMP_BASE + ["v3", "engineid", want_eid])) if state in ("replaced", "overridden") and have_eid and want_eid != have_eid: cmds.append(_delete(SNMP_BASE + ["v3", "engineid"])) cmds += _build_v3_group_commands(want_v3.get("groups"), have_v3.get("groups"), state) cmds += _build_v3_user_commands(want_v3.get("users"), have_v3.get("users"), state) cmds += _build_v3_view_commands(want_v3.get("views"), have_v3.get("views"), state) return cmds def build_commands(want, have, state): """ Build the full list of API command dicts to move from have → want. For deleted state, a single delete of the entire SNMP subtree is issued. """ if state == "deleted": if have: return [_delete(SNMP_BASE)] return [] cmds = [] cmds += _build_scalar_commands(want, have, state) cmds += _build_community_commands(want.get("communities"), have.get("communities"), state) cmds += _build_listen_address_commands( want.get("listen_addresses"), have.get("listen_addresses"), state, ) cmds += _build_trap_target_commands(want.get("trap_target"), have.get("trap_target"), state) cmds += _build_v3_commands(want.get("snmp_v3"), have.get("snmp_v3"), state) return cmds # ------------------------------------------------------------ # Argument spec # ------------------------------------------------------------ def _auth_privacy_spec(): return dict( type=dict(type="str"), encrypted_key=dict(type="str", no_log=False), plaintext_key=dict(type="str", no_log=True), ) ARGUMENT_SPEC = dict( config=dict( type="dict", options=dict( communities=dict( type="list", elements="dict", options=dict( name=dict(type="str", required=True), clients=dict(type="list", elements="str"), networks=dict(type="list", elements="str"), authorization_type=dict(type="str", choices=["ro", "rw"]), ), ), contact=dict(type="str"), description=dict(type="str"), location=dict(type="str"), smux_peer=dict(type="str"), trap_source=dict(type="str"), listen_addresses=dict( type="list", elements="dict", options=dict( address=dict(type="str", required=True), port=dict(type="int"), ), ), trap_target=dict( type="dict", options=dict( address=dict(type="str"), community=dict(type="str"), port=dict(type="int"), ), ), snmp_v3=dict( type="dict", options=dict( engine_id=dict(type="str"), groups=dict( type="list", elements="dict", options=dict( group=dict(type="str", required=True), mode=dict(type="str", choices=["ro", "rw"]), seclevel=dict(type="str", choices=["auth", "priv"]), view=dict(type="str"), ), ), users=dict( type="list", elements="dict", options=dict( user=dict(type="str", required=True), authentication=dict(type="dict", options=_auth_privacy_spec()), privacy=dict(type="dict", options=_auth_privacy_spec()), group=dict(type="str"), mode=dict(type="str", choices=["ro", "rw"]), tsm_key=dict(type="str"), ), ), trap_targets=dict( type="list", elements="dict", options=dict( address=dict(type="str"), port=dict(type="int"), protocol=dict(type="str", choices=["tcp", "udp"]), type=dict(type="str", choices=["inform", "trap"]), authentication=dict(type="dict", options=_auth_privacy_spec()), privacy=dict(type="dict", options=_auth_privacy_spec()), ), ), views=dict( type="list", elements="dict", options=dict( view=dict(type="str", required=True), oid=dict(type="str"), exclude=dict(type="str"), mask=dict(type="str"), ), ), ), ), ), ), state=dict( type="str", default="merged", choices=["merged", "replaced", "overridden", "deleted", "gathered"], ), ) # ------------------------------------------------------------ # Main # ------------------------------------------------------------ def main(): module = AnsibleModule( argument_spec=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) want = config # already in argspec shape from AnsibleModule commands = build_commands(want, 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=want, commands=commands, saved=saved, response=response, ) module.exit_json(changed=False, before=have, after=have, commands=[]) if __name__ == "__main__": main()