diff options
Diffstat (limited to 'ansible_collections/cisco/nxos/plugins/modules')
92 files changed, 45790 insertions, 0 deletions
diff --git a/ansible_collections/cisco/nxos/plugins/modules/__init__.py b/ansible_collections/cisco/nxos/plugins/modules/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/__init__.py diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server.py new file mode 100644 index 00000000..9103f931 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_aaa_server +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages AAA server global configuration. +description: +- Manages AAA server global configuration +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- The server_type parameter is always required. +- If encrypt_type is not supplied, the global AAA server key will be stored as encrypted + (type 7). +- Changes to the global AAA server key with encrypt_type=0 are not idempotent. +- state=default will set the supplied parameters to their default values. The parameters + that you want to default must also be set to default. If global_key=default, the + global key will be removed. +options: + server_type: + description: + - The server type is either radius or tacacs. + required: true + choices: + - radius + - tacacs + type: str + global_key: + description: + - Global AAA shared secret or keyword 'default'. + type: str + encrypt_type: + description: + - The state of encryption applied to the entered global key. O clear text, 7 encrypted. + Type-6 encryption is not supported. + choices: + - '0' + - '7' + type: str + deadtime: + description: + - Duration for which a non-reachable AAA server is skipped, in minutes or keyword + 'default. Range is 1-1440. Device default is 0. + type: str + server_timeout: + description: + - Global AAA server timeout period, in seconds or keyword 'default. Range is 1-60. + Device default is 5. + type: str + directed_request: + description: + - Enables direct authentication requests to AAA server or keyword 'default' Device + default is disabled. + choices: + - enabled + - disabled + - default + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - default + type: str +""" + +EXAMPLES = """ +# Radius Server Basic settings +- name: Radius Server Basic settings + cisco.nxos.nxos_aaa_server: + server_type: radius + server_timeout: 9 + deadtime: 20 + directed_request: enabled + +# Tacacs Server Basic settings +- name: Tacacs Server Basic settings + cisco.nxos.nxos_aaa_server: + server_type: tacacs + server_timeout: 8 + deadtime: 19 + directed_request: disabled + +# Setting Global Key +- name: AAA Server Global Key + cisco.nxos.nxos_aaa_server: + server_type: radius + global_key: test_key +""" + +RETURN = """ +commands: + description: command sent to the device + returned: always + type: list + sample: ["radius-server deadtime 22", "radius-server timeout 11", + "radius-server directed-request"] +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +PARAM_TO_DEFAULT_KEYMAP = { + "server_timeout": "5", + "deadtime": "0", + "directed_request": "disabled", +} + + +def execute_show_command(command, module): + command = {"command": command, "output": "text"} + + return run_commands(module, command) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_aaa_server_info(server_type, module): + aaa_server_info = {} + server_command = "show {0}-server".format(server_type) + request_command = "show {0}-server directed-request".format(server_type) + global_key_command = "show run | sec {0}".format(server_type) + aaa_regex = r".*{0}-server\skey\s\d\s+(?P<key>\S+).*".format(server_type) + + server_body = execute_show_command(server_command, module)[0] + + split_server = server_body.splitlines() + + for line in split_server: + if line.startswith("timeout"): + aaa_server_info["server_timeout"] = line.split(":")[1] + + elif line.startswith("deadtime"): + aaa_server_info["deadtime"] = line.split(":")[1] + + request_body = execute_show_command(request_command, module)[0] + + if bool(request_body): + aaa_server_info["directed_request"] = request_body.replace("\n", "") + else: + aaa_server_info["directed_request"] = "disabled" + + key_body = execute_show_command(global_key_command, module)[0] + + try: + match_global_key = re.match(aaa_regex, key_body, re.DOTALL) + group_key = match_global_key.groupdict() + aaa_server_info["global_key"] = group_key["key"].replace('"', "") + except (AttributeError, TypeError): + aaa_server_info["global_key"] = None + + return aaa_server_info + + +def config_aaa_server(params, server_type): + cmds = [] + + deadtime = params.get("deadtime") + server_timeout = params.get("server_timeout") + directed_request = params.get("directed_request") + encrypt_type = params.get("encrypt_type", "7") + global_key = params.get("global_key") + + if deadtime is not None: + cmds.append("{0}-server deadtime {1}".format(server_type, deadtime)) + + if server_timeout is not None: + cmds.append("{0}-server timeout {1}".format(server_type, server_timeout)) + + if directed_request is not None: + if directed_request == "enabled": + cmds.append("{0}-server directed-request".format(server_type)) + elif directed_request == "disabled": + cmds.append("no {0}-server directed-request".format(server_type)) + + if global_key is not None: + cmds.append("{0}-server key {1} {2}".format(server_type, encrypt_type, global_key)) + + return cmds + + +def default_aaa_server(existing, params, server_type): + cmds = [] + + deadtime = params.get("deadtime") + server_timeout = params.get("server_timeout") + directed_request = params.get("directed_request") + global_key = params.get("global_key") + existing_key = existing.get("global_key") + + if deadtime is not None and existing.get("deadtime") != PARAM_TO_DEFAULT_KEYMAP["deadtime"]: + cmds.append("no {0}-server deadtime 1".format(server_type)) + + if ( + server_timeout is not None + and existing.get("server_timeout") != PARAM_TO_DEFAULT_KEYMAP["server_timeout"] + ): + cmds.append("no {0}-server timeout 1".format(server_type)) + + if ( + directed_request is not None + and existing.get("directed_request") != PARAM_TO_DEFAULT_KEYMAP["directed_request"] + ): + cmds.append("no {0}-server directed-request".format(server_type)) + + if global_key is not None and existing_key is not None: + cmds.append("no {0}-server key 7 {1}".format(server_type, existing_key)) + + return cmds + + +def main(): + argument_spec = dict( + server_type=dict(type="str", choices=["radius", "tacacs"], required=True), + global_key=dict(type="str", no_log=True), + encrypt_type=dict(type="str", choices=["0", "7"]), + deadtime=dict(type="str"), + server_timeout=dict(type="str"), + directed_request=dict(type="str", choices=["enabled", "disabled", "default"]), + state=dict(choices=["default", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + server_type = module.params["server_type"] + global_key = module.params["global_key"] + encrypt_type = module.params["encrypt_type"] + deadtime = module.params["deadtime"] + server_timeout = module.params["server_timeout"] + directed_request = module.params["directed_request"] + state = module.params["state"] + + if encrypt_type and not global_key: + module.fail_json(msg="encrypt_type must be used with global_key.") + + args = dict( + server_type=server_type, + global_key=global_key, + encrypt_type=encrypt_type, + deadtime=deadtime, + server_timeout=server_timeout, + directed_request=directed_request, + ) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + + existing = get_aaa_server_info(server_type, module) + + commands = [] + if state == "present": + if deadtime: + try: + if int(deadtime) < 0 or int(deadtime) > 1440: + raise ValueError + except ValueError: + module.fail_json(msg="deadtime must be an integer between 0 and 1440") + + if server_timeout: + try: + if int(server_timeout) < 1 or int(server_timeout) > 60: + raise ValueError + except ValueError: + module.fail_json(msg="server_timeout must be an integer between 1 and 60") + + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = config_aaa_server(delta, server_type) + if command: + commands.append(command) + + elif state == "default": + for key, value in proposed.items(): + if key != "server_type" and value != "default": + module.fail_json(msg='Parameters must be set to "default"' "when state=default") + command = default_aaa_server(existing, proposed, server_type) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server_host.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server_host.py new file mode 100644 index 00000000..d2f84f76 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_aaa_server_host.py @@ -0,0 +1,370 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_aaa_server_host +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages AAA server host-specific configuration. +description: +- Manages AAA server host-specific configuration. +version_added: 1.0.0 +author: Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- Changes to the host key (shared secret) are not idempotent for type 0. +- If C(state=absent) removes the whole host configuration. +options: + server_type: + description: + - The server type is either radius or tacacs. + required: true + choices: + - radius + - tacacs + type: str + address: + description: + - Address or name of the radius or tacacs host. + required: true + type: str + key: + description: + - Shared secret for the specified host or keyword 'default'. + type: str + encrypt_type: + description: + - The state of encryption applied to the entered key. O for clear text, 7 for + encrypted. Type-6 encryption is not supported. + choices: + - '0' + - '7' + type: str + host_timeout: + description: + - Timeout period for specified host, in seconds or keyword 'default. Range is + 1-60. + type: str + auth_port: + description: + - Alternate UDP port for RADIUS authentication or keyword 'default'. + type: str + acct_port: + description: + - Alternate UDP port for RADIUS accounting or keyword 'default'. + type: str + tacacs_port: + description: + - Alternate TCP port TACACS Server or keyword 'default'. + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" +EXAMPLES = """ +# Radius Server Host Basic settings +- name: Radius Server Host Basic settings + cisco.nxos.nxos_aaa_server_host: + state: present + server_type: radius + address: 1.2.3.4 + acct_port: 2084 + host_timeout: 10 + +# Radius Server Host Key Configuration +- name: Radius Server Host Key Configuration + cisco.nxos.nxos_aaa_server_host: + state: present + server_type: radius + address: 1.2.3.4 + key: hello + encrypt_type: 7 + +# TACACS Server Host Configuration +- name: Tacacs Server Host Configuration + cisco.nxos.nxos_aaa_server_host: + state: present + server_type: tacacs + tacacs_port: 89 + host_timeout: 10 + address: 5.6.7.8 + +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"address": "1.2.3.4", "auth_port": "2084", + "host_timeout": "10", "server_type": "radius"} +existing: + description: + - k/v pairs of existing configuration + returned: always + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"address": "1.2.3.4", "auth_port": "2084", + "host_timeout": "10", "server_type": "radius"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["radius-server host 1.2.3.4 auth-port 2084 timeout 10"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) + + +def execute_show_command(command, module): + device_info = get_capabilities(module) + network_api = device_info.get("network_api", "nxapi") + + if network_api == "cliconf": + cmds = [command] + body = run_commands(module, cmds) + elif network_api == "nxapi": + cmds = {"command": command, "output": "text"} + body = run_commands(module, cmds) + + return body + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_aaa_host_info(module, server_type, address): + aaa_host_info = {} + command = "show run | inc {0}-server.host.{1}".format(server_type, address) + + body = execute_show_command(command, module)[0] + if body: + try: + if "radius" in body: + pattern = ( + r"\S+ host \S+(?:\s+key 7\s+(\S+))?(?:\s+auth-port (\d+))?" + r"(?:\s+acct-port (\d+))?(?:\s+authentication)?" + r"(?:\s+accounting)?(?:\s+timeout (\d+))?" + ) + match = re.search(pattern, body) + aaa_host_info["key"] = match.group(1) + if aaa_host_info["key"]: + aaa_host_info["key"] = aaa_host_info["key"].replace('"', "") + aaa_host_info["encrypt_type"] = "7" + aaa_host_info["auth_port"] = match.group(2) + aaa_host_info["acct_port"] = match.group(3) + aaa_host_info["host_timeout"] = match.group(4) + elif "tacacs" in body: + pattern = ( + r"\S+ host \S+(?:\s+key 7\s+(\S+))?(?:\s+port (\d+))?(?:\s+timeout (\d+))?" + ) + match = re.search(pattern, body) + aaa_host_info["key"] = match.group(1) + if aaa_host_info["key"]: + aaa_host_info["key"] = aaa_host_info["key"].replace('"', "") + aaa_host_info["encrypt_type"] = "7" + aaa_host_info["tacacs_port"] = match.group(2) + aaa_host_info["host_timeout"] = match.group(3) + + aaa_host_info["server_type"] = server_type + aaa_host_info["address"] = address + except TypeError: + return {} + else: + return {} + + return aaa_host_info + + +def config_aaa_host(server_type, address, params, existing): + cmds = [] + cmd_str = "{0}-server host {1}".format(server_type, address) + cmd_no_str = "no " + cmd_str + + key = params.get("key") + enc_type = params.get("encrypt_type", "") + + defval = False + nondef = False + + if key: + if key != "default": + cmds.append(cmd_str + " key {0} {1}".format(enc_type, key)) + else: + cmds.append(cmd_no_str + " key 7 {0}".format(existing.get("key"))) + + locdict = { + "auth_port": "auth-port", + "acct_port": "acct-port", + "tacacs_port": "port", + "host_timeout": "timeout", + } + + # platform CLI needs the keywords in the following order + for key in ["auth_port", "acct_port", "tacacs_port", "host_timeout"]: + item = params.get(key) + if item: + if item != "default": + cmd_str += " {0} {1}".format(locdict.get(key), item) + nondef = True + else: + cmd_no_str += " {0} 1".format(locdict.get(key)) + defval = True + if defval: + cmds.append(cmd_no_str) + if nondef or not existing: + cmds.append(cmd_str) + + return cmds + + +def main(): + argument_spec = dict( + server_type=dict(choices=["radius", "tacacs"], required=True), + address=dict(type="str", required=True), + key=dict(type="str", no_log=False), + encrypt_type=dict(type="str", choices=["0", "7"]), + host_timeout=dict(type="str"), + auth_port=dict(type="str"), + acct_port=dict(type="str"), + tacacs_port=dict(type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + server_type = module.params["server_type"] + address = module.params["address"] + key = module.params["key"] + encrypt_type = module.params["encrypt_type"] + host_timeout = module.params["host_timeout"] + auth_port = module.params["auth_port"] + acct_port = module.params["acct_port"] + tacacs_port = module.params["tacacs_port"] + state = module.params["state"] + + args = dict( + server_type=server_type, + address=address, + key=key, + encrypt_type=encrypt_type, + host_timeout=host_timeout, + auth_port=auth_port, + acct_port=acct_port, + tacacs_port=tacacs_port, + ) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + changed = False + + if encrypt_type and not key: + module.fail_json(msg="encrypt_type must be used with key") + + if tacacs_port and server_type != "tacacs": + module.fail_json(msg="tacacs_port can only be used with server_type=tacacs") + + if (auth_port or acct_port) and server_type != "radius": + module.fail_json(msg="auth_port and acct_port can only be used" "when server_type=radius") + + existing = get_aaa_host_info(module, server_type, address) + end_state = existing + + commands = [] + delta = {} + if state == "present": + if not existing: + delta = proposed + else: + for key, value in proposed.items(): + if key == "encrypt_type": + delta[key] = value + if value != existing.get(key): + if value != "default" or existing.get(key): + delta[key] = value + + command = config_aaa_host(server_type, address, delta, existing) + if command: + commands.append(command) + + elif state == "absent": + intersect = dict(set(proposed.items()).intersection(existing.items())) + if intersect.get("address") and intersect.get("server_type"): + command = "no {0}-server host {1}".format( + intersect.get("server_type"), + intersect.get("address"), + ) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_aaa_host_info(module, server_type, address) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + results["end_state"] = end_state + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_acl_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_acl_interfaces.py new file mode 100644 index 00000000..e61746ff --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_acl_interfaces.py @@ -0,0 +1,440 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_acl_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_acl_interfaces +short_description: ACL interfaces resource module +description: Add and remove Access Control Lists on interfaces in NX-OS platform +version_added: 1.0.0 +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: +- Tested against NX-OS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^interface'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of interfaces to be configured with ACLs + type: list + elements: dict + suboptions: + name: + description: Name of the interface + type: str + required: true + access_groups: + description: List of address family indicators with ACLs to be configured + on the interface + type: list + elements: dict + suboptions: + afi: + description: Address Family Indicator of the ACLs to be configured + type: str + required: true + choices: + - ipv4 + - ipv6 + acls: + description: List of Access Control Lists for the interface + type: list + elements: dict + suboptions: + name: + description: Name of the ACL to be added/removed + type: str + required: true + direction: + description: Direction to be applied for the ACL + type: str + required: true + choices: + - in + - out + port: + description: Use ACL as port policy. + type: bool + state: + description: The state the configuration should be left in + type: str + choices: + - deleted + - gathered + - merged + - overridden + - rendered + - replaced + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------ +# + +- name: Merge ACL interfaces configuration + cisco.nxos.nxos_acl_interfaces: + config: + - name: Ethernet1/2 + access_groups: + - afi: ipv6 + acls: + - name: ACL1v6 + direction: in + + - name: Eth1/5 + access_groups: + - afi: ipv4 + acls: + - name: PortACL + direction: in + port: true + + - name: ACL1v4 + direction: out + + - afi: ipv6 + acls: + - name: ACL1v6 + direction: in + state: merged + +# After state: +# ------------ +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ip port access-group PortACL in +# ip access-group ACL1v4 out +# ipv6 traffic-filter ACL1v6 in + +# Using replaced + +# Before state: +# ------------ +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ip port access-group PortACL in +# ip access-group ACL1v4 out +# ipv6 traffic-filter ACL1v6 in + +- name: Replace interface configuration with given configuration + cisco.nxos.nxos_acl_interfaces: + config: + - name: Eth1/5 + access_groups: + - afi: ipv4 + acls: + - name: NewACLv4 + direction: out + + - name: Ethernet1/3 + access_groups: + - afi: ipv6 + acls: + - name: NewACLv6 + direction: in + port: true + state: replaced + +# After state: +# ------------ +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/3 +# ipv6 port traffic-filter NewACLv6 in +# interface Ethernet1/5 +# ip access-group NewACLv4 out + +# Using overridden + +# Before state: +# ------------ +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ip port access-group PortACL in +# ip access-group ACL1v4 out +# ipv6 traffic-filter ACL1v6 in + +- name: Override interface configuration with given configuration + cisco.nxos.nxos_acl_interfaces: + config: + - name: Ethernet1/3 + access_groups: + - afi: ipv4 + acls: + - name: ACL1v4 + direction: out + + - name: PortACL + port: true + direction: in + - afi: ipv6 + acls: + - name: NewACLv6 + direction: in + port: true + state: overridden + +# After state: +# ------------ +# interface Ethernet1/3 +# ip access-group ACL1v4 out +# ip port access-group PortACL in +# ipv6 port traffic-filter NewACLv6 in + +# Using deleted to remove ACL config from specified interfaces + +# Before state: +# ------------- +# interface Ethernet1/1 +# ip access-group ACL2v4 in +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ip port access-group PortACL in +# ip access-group ACL1v4 out +# ipv6 traffic-filter ACL1v6 in + +- name: Delete ACL configuration on interfaces + cisco.nxos.nxos_acl_interfaces: + config: + - name: Ethernet1/5 + - name: Ethernet1/2 + state: deleted + +# After state: +# ------------- +# interface Ethernet1/1 +# ip access-group ACL2v4 in +# interface Ethernet1/2 +# interface Ethernet1/5 + +# Using deleted to remove ACL config from all interfaces + +# Before state: +# ------------- +# interface Ethernet1/1 +# ip access-group ACL2v4 in +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ip port access-group PortACL in +# ip access-group ACL1v4 out +# ipv6 traffic-filter ACL1v6 in + +- name: Delete ACL configuration from all interfaces + cisco.nxos.nxos_acl_interfaces: + state: deleted + +# After state: +# ------------- +# interface Ethernet1/1 +# interface Ethernet1/2 +# interface Ethernet1/5 + +# Using parsed + +- name: Parse given configuration into structured format + cisco.nxos.nxos_acl_interfaces: + running_config: | + interface Ethernet1/2 + ipv6 traffic-filter ACL1v6 in + interface Ethernet1/5 + ipv6 traffic-filter ACL1v6 in + ip access-group ACL1v4 out + ip port access-group PortACL in + state: parsed + +# returns +# parsed: +# - name: Ethernet1/2 +# access_groups: +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# direction: in +# - name: Ethernet1/5 +# access_groups: +# - afi: ipv4 +# acls: +# - name: PortACL +# direction: in +# port: True +# - name: ACL1v4 +# direction: out +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# direction: in + + +# Using gathered: + +# Before state: +# ------------ +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ipv6 traffic-filter ACL1v6 in +# ip access-group ACL1v4 out +# ip port access-group PortACL in + +- name: Gather existing configuration from device + cisco.nxos.nxos_acl_interfaces: + config: + state: gathered + +# returns +# gathered: +# - name: Ethernet1/2 +# access_groups: +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# direction: in +# - name: Ethernet1/5 +# access_groups: +# - afi: ipv4 +# acls: +# - name: PortACL +# direction: in +# port: True +# - name: ACL1v4 +# direction: out +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# direction: in + + +# Using rendered + +- name: Render required configuration to be pushed to the device + cisco.nxos.nxos_acl_interfaces: + config: + - name: Ethernet1/2 + access_groups: + - afi: ipv6 + acls: + - name: ACL1v6 + direction: in + + - name: Ethernet1/5 + access_groups: + - afi: ipv4 + acls: + - name: PortACL + direction: in + port: true + - name: ACL1v4 + direction: out + - afi: ipv6 + acls: + - name: ACL1v6 + direction: in + state: rendered + +# returns +# rendered: +# interface Ethernet1/2 +# ipv6 traffic-filter ACL1v6 in +# interface Ethernet1/5 +# ipv6 traffic-filter ACL1v6 in +# ip access-group ACL1v4 out +# ip port access-group PortACL in + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/2', 'ipv6 traffic-filter ACL1v6 out', 'ip port access-group PortACL in'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.acl_interfaces.acl_interfaces import ( + Acl_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.acl_interfaces.acl_interfaces import ( + Acl_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Acl_interfacesArgs.argument_spec, + supports_check_mode=True, + ) + + result = Acl_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_acls.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_acls.py new file mode 100644 index 00000000..51a66504 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_acls.py @@ -0,0 +1,915 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The module file for nxos_acls +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_acls +short_description: ACLs resource module +description: Manage named IP ACLs on the Cisco NX-OS platform +version_added: 1.0.0 +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: +- Tested against NX-OS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- As NX-OS allows configuring a rule again with different sequence numbers, the user + is expected to provide sequence numbers for the access control entries to preserve + idempotency. If no sequence number is given, the rule will be added as a new rule + by the device. +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section 'ip(v6)* access-list). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of ACL options. + type: list + elements: dict + suboptions: + afi: + description: The Address Family Indicator (AFI) for the ACL. + type: str + required: true + choices: + - ipv4 + - ipv6 + acls: + description: A list of the ACLs. + type: list + elements: dict + suboptions: + name: + description: Name of the ACL. + type: str + required: true + aces: + description: The entries within the ACL. + type: list + elements: dict + suboptions: + grant: + description: Action to be applied on the rule. + type: str + choices: + - permit + - deny + destination: + description: Specify the packet destination. + type: dict + suboptions: + address: + description: Destination network address. + type: str + any: + description: Any destination address. + type: bool + host: + description: Host IP address. + type: str + port_protocol: + description: Specify the destination port or protocol (only for + TCP and UDP). + type: dict + suboptions: + eq: + description: Match only packets on a given port number. + type: str + gt: + description: Match only packets with a greater port number. + type: str + lt: + description: Match only packets with a lower port number. + type: str + neq: + description: Match only packets not on a given port number. + type: str + range: + description: Match only packets in the range of port numbers. + type: dict + suboptions: + start: + description: Specify the start of the port range. + type: str + end: + description: Specify the end of the port range. + type: str + prefix: + description: Destination network prefix. Only for prefixes of + value less than 31 for ipv4 and 127 for ipv6. Prefixes of 32 + (ipv4) and 128 (ipv6) should be given in the 'host' key. + type: str + wildcard_bits: + description: Destination wildcard bits. + type: str + dscp: + description: Match packets with given DSCP value. + type: str + fragments: + description: Check non-initial fragments. + type: bool + remark: + description: Access list entry comment. + type: str + sequence: + description: Sequence number. + type: int + source: + description: Specify the packet source. + type: dict + suboptions: + address: + description: Source network address. + type: str + any: + description: Any source address. + type: bool + host: + description: Host IP address. + type: str + port_protocol: + description: Specify the destination port or protocol (only for + TCP and UDP). + type: dict + suboptions: + eq: + description: Match only packets on a given port number. + type: str + gt: + description: Match only packets with a greater port number. + type: str + lt: + description: Match only packets with a lower port number. + type: str + neq: + description: Match only packets not on a given port number. + type: str + range: + description: Match only packets in the range of port numbers. + type: dict + suboptions: + start: + description: Specify the start of the port range. + type: str + end: + description: Specify the end of the port range. + type: str + prefix: + description: Source network prefix. Only for prefixes of mask + value less than 31 for ipv4 and 127 for ipv6. Prefixes of mask + 32 (ipv4) and 128 (ipv6) should be given in the 'host' key. + type: str + wildcard_bits: + description: Source wildcard bits. + type: str + log: + description: Log matches against this entry. + type: bool + precedence: + description: Match packets with given precedence value. + type: str + protocol: + description: Specify the protocol. + type: str + protocol_options: + description: All possible suboptions for the protocol chosen. + type: dict + suboptions: + icmp: + description: ICMP protocol options. + type: dict + suboptions: + administratively_prohibited: + description: Administratively prohibited + type: bool + alternate_address: + description: Alternate address + type: bool + conversion_error: + description: Datagram conversion + type: bool + dod_host_prohibited: + description: Host prohibited + type: bool + dod_net_prohibited: + description: Net prohibited + type: bool + echo: + description: Echo (ping) + type: bool + echo_reply: + description: Echo reply + type: bool + echo_request: + description: Echo request (ping) + type: bool + general_parameter_problem: + description: Parameter problem + type: bool + host_isolated: + description: Host isolated + type: bool + host_precedence_unreachable: + description: Host unreachable for precedence + type: bool + host_redirect: + description: Host redirect + type: bool + host_tos_redirect: + description: Host redirect for TOS + type: bool + host_tos_unreachable: + description: Host unreachable for TOS + type: bool + host_unknown: + description: Host unknown + type: bool + host_unreachable: + description: Host unreachable + type: bool + information_reply: + description: Information replies + type: bool + information_request: + description: Information requests + type: bool + mask_reply: + description: Mask replies + type: bool + mask_request: + description: Mask requests + type: bool + message_code: + description: ICMP message code + type: int + message_type: + description: ICMP message type + type: int + mobile_redirect: + description: Mobile host redirect + type: bool + net_redirect: + description: Network redirect + type: bool + net_tos_redirect: + description: Net redirect for TOS + type: bool + net_tos_unreachable: + description: Network unreachable for TOS + type: bool + net_unreachable: + description: Net unreachable + type: bool + network_unknown: + description: Network unknown + type: bool + no_room_for_option: + description: Parameter required but no room + type: bool + option_missing: + description: Parameter required but not present + type: bool + packet_too_big: + description: Fragmentation needed and DF set + type: bool + parameter_problem: + description: All parameter problems + type: bool + port_unreachable: + description: Port unreachable + type: bool + precedence_unreachable: + description: Precedence cutoff + type: bool + protocol_unreachable: + description: Protocol unreachable + type: bool + reassembly_timeout: + description: Reassembly timeout + type: bool + redirect: + description: All redirects + type: bool + router_advertisement: + description: Router discovery advertisements + type: bool + router_solicitation: + description: Router discovery solicitations + type: bool + source_quench: + description: Source quenches + type: bool + source_route_failed: + description: Source route failed + type: bool + time_exceeded: + description: All time exceeded. + type: bool + timestamp_reply: + description: Timestamp replies + type: bool + timestamp_request: + description: Timestamp requests + type: bool + traceroute: + description: Traceroute + type: bool + ttl_exceeded: + description: TTL exceeded + type: bool + unreachable: + description: All unreachables + type: bool + icmpv6: + description: ICMPv6 protocol options. + type: dict + suboptions: + beyond_scope: + description: Destination beyond scope. + type: bool + destination_unreachable: + description: Destination address is unreachable. + type: bool + echo_reply: + description: Echo reply. + type: bool + echo_request: + description: Echo request (ping). + type: bool + fragments: + description: Check non-initial fragments. + type: bool + header: + description: Parameter header problem. + type: bool + hop_limit: + description: Hop limit exceeded in transit. + type: bool + mld_query: + description: Multicast Listener Discovery Query. + type: bool + mld_reduction: + description: Multicast Listener Discovery Reduction. + type: bool + mld_report: + description: Multicast Listener Discovery Report. + type: bool + mldv2: + description: Multicast Listener Discovery Protocol. + type: bool + nd_na: + description: Neighbor discovery neighbor advertisements. + type: bool + nd_ns: + description: Neighbor discovery neighbor solicitations. + type: bool + next_header: + description: Parameter next header problems. + type: bool + no_admin: + description: Administration prohibited destination. + type: bool + no_route: + description: No route to destination. + type: bool + packet_too_big: + description: Packet too big. + type: bool + parameter_option: + description: Parameter option problems. + type: bool + parameter_problem: + description: All parameter problems. + type: bool + port_unreachable: + description: Port unreachable. + type: bool + reassembly_timeout: + description: Reassembly timeout. + type: bool + renum_command: + description: Router renumbering command. + type: bool + renum_result: + description: Router renumbering result. + type: bool + renum_seq_number: + description: Router renumbering sequence number reset. + type: bool + router_advertisement: + description: Neighbor discovery router advertisements. + type: bool + router_renumbering: + description: All router renumbering. + type: bool + router_solicitation: + description: Neighbor discovery router solicitations. + type: bool + time_exceeded: + description: All time exceeded. + type: bool + unreachable: + description: All unreachable. + type: bool + telemetry_path: + description: IPT enabled. + type: bool + telemetry_queue: + description: Flow of interest for BDC/HDC. + type: bool + tcp: + description: TCP flags. + type: dict + suboptions: + ack: + description: Match on the ACK bit + type: bool + established: + description: Match established connections + type: bool + fin: + description: Match on the FIN bit + type: bool + psh: + description: Match on the PSH bit + type: bool + rst: + description: Match on the RST bit + type: bool + syn: + description: Match on the SYN bit + type: bool + urg: + description: Match on the URG bit + type: bool + igmp: + description: IGMP protocol options. + type: dict + suboptions: + dvmrp: + description: Distance Vector Multicast Routing Protocol + type: bool + host_query: + description: Host Query + type: bool + host_report: + description: Host Report + type: bool + state: + description: + - The state the configuration should be left in + type: str + choices: + - deleted + - gathered + - merged + - overridden + - rendered + - replaced + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# + +- name: Merge new ACLs configuration + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 55 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + prefix: 2001:db8:12::/32 + protocol: sctp + state: merged + +# After state: +# ------------ +# +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any + +# Using replaced + +# Before state: +# ---------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Replace existing ACL configuration with provided configuration + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - sequence: 20 + grant: permit + source: + any: true + destination: + any: true + protocol: pip + + - remark: Replaced ACE + + - name: ACL2v6 + state: replaced + +# After state: +# --------------- +# +# ipv6 access-list ACL1v6 +# 20 permit pip any any +# 30 remark Replaced ACE +# ipv6 access-list ACL2v6 + +# Using overridden + +# Before state: +# ---------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Override existing configuration with provided configuration + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + acls: + - name: NewACL + aces: + - grant: deny + source: + address: 192.0.2.0 + wildcard_bits: 0.0.255.255 + destination: + any: true + protocol: eigrp + - remark: Example for overridden state + state: overridden + +# After state: +# ------------ +# +# ip access-list NewACL +# 10 deny eigrp 192.0.2.0 0.0.255.255 any +# 20 remark Example for overridden state + +# Using deleted: +# +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete all ACLs + cisco.nxos.nxos_acls: + config: + state: deleted + +# After state: +# ----------- +# + + +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete all ACLs in given AFI + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + state: deleted + +# After state: +# ------------ +# +# ip access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ip access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + + + +# Before state: +# ------------- +# +# ip access-list ACL1v4 +# 10 permit ip any any +# 20 deny udp any any +# ip access-list ACL2v4 +# 10 permit ahp 192.0.2.0 0.0.0.255 any +# ipv6 access-list ACL1v6 +# 10 permit sctp any any +# 20 remark IPv6 ACL +# ipv6 access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +- name: Delete specific ACLs + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + - name: ACL2v4 + - afi: ipv6 + acls: + - name: ACL1v6 + state: deleted + +# After state: +# ------------ +# ipv6 access-list ACL2v6 +# 10 deny ipv6 any 2001:db8:3000::/36 +# 20 permit tcp 2001:db8:2000:2::2/128 2001:db8:2000:ab::2/128 + +# Using parsed + +- name: Parse given config to structured data + cisco.nxos.nxos_acls: + running_config: | + ip access-list ACL1v4 + 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin + ipv6 access-list ACL1v6 + 10 permit sctp any any + state: parsed + +# returns: +# parsed: +# - afi: ipv4 +# acls: +# - name: ACL1v4 +# aces: +# - grant: deny +# destination: +# address: 192.0.2.64 +# wildcard_bits: 0.0.0.255 +# source: +# any: true +# port_protocol: +# lt: 55 +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# fin: true +# sequence: 50 +# +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# aces: +# - grant: permit +# sequence: 10 +# source: +# any: true +# destination: +# prefix: 2001:db8:12::/32 +# protocol: sctp + + +# Using gathered: + +# Before state: +# ------------ +# +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any + +- name: Gather existing configuration + cisco.nxos.nxos_acls: + state: gathered + +# returns: +# gathered: +# - afi: ipv4 +# acls: +# - name: ACL1v4 +# aces: +# - grant: deny +# destination: +# address: 192.0.2.64 +# wildcard_bits: 0.0.0.255 +# source: +# any: true +# port_protocol: +# lt: 55 +# protocol: tcp +# protocol_options: +# tcp: +# ack: true +# fin: true +# sequence: 50 + +# - afi: ipv6 +# acls: +# - name: ACL1v6 +# aces: +# - grant: permit +# sequence: 10 +# source: +# any: true +# destination: +# prefix: 2001:db8:12::/32 +# protocol: sctp + + +# Using rendered + +- name: Render required configuration to be pushed to the device + cisco.nxos.nxos_acls: + config: + - afi: ipv4 + acls: + - name: ACL1v4 + aces: + - grant: deny + destination: + address: 192.0.2.64 + wildcard_bits: 0.0.0.255 + source: + any: true + port_protocol: + lt: 55 + protocol: tcp + protocol_options: + tcp: + ack: true + fin: true + sequence: 50 + + - afi: ipv6 + acls: + - name: ACL1v6 + aces: + - grant: permit + sequence: 10 + source: + any: true + destination: + prefix: 2001:db8:12::/32 + protocol: sctp + state: rendered + +# returns: +# rendered: +# ip access-list ACL1v4 +# 50 deny tcp any lt 55 192.0.2.64 0.0.0.255 ack fin +# ipv6 access-list ACL1v6 +# 10 permit sctp any any +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['ip access-list ACL1v4', '10 permit ip any any precedence critical log', '20 deny tcp any lt smtp host 192.0.2.64 ack fin'] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.acls.acls import ( + AclsArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.acls.acls import Acls + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=AclsArgs.argument_spec, supports_check_mode=True) + + result = Acls(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_banner.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_banner.py new file mode 100644 index 00000000..f946dc0a --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_banner.py @@ -0,0 +1,224 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# (c) 2017, Ansible by Red Hat, inc +# +# This file is part of Ansible by Red Hat +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + + +DOCUMENTATION = """ +module: nxos_banner +author: Trishna Guha (@trishnaguha) +short_description: Manage multiline banners on Cisco NXOS devices +description: +- This will configure both exec and motd banners on remote devices running Cisco NXOS. + It allows playbooks to add or remove banner text from the active running configuration. +notes: +- Since responses from the device are always read with surrounding whitespaces stripped, + tasks that configure banners with preceeding or trailing whitespaces will not be idempotent. +- Limited Support for Cisco MDS +version_added: 1.0.0 +options: + banner: + description: + - Specifies which banner that should be configured on the remote device. + required: true + choices: + - exec + - motd + type: str + text: + description: + - The banner text that should be present in the remote device running configuration. + This argument accepts a multiline string, with no empty lines. Requires I(state=present). + type: str + state: + description: + - Specifies whether or not the configuration is present in the current devices + active running configuration. + default: present + choices: + - present + - absent + type: str +extends_documentation_fragment: +- cisco.nxos.nxos +""" + +EXAMPLES = """ +- name: configure the exec banner + cisco.nxos.nxos_banner: + banner: exec + text: | + this is my exec banner + that contains a multiline + string + state: present +- name: remove the motd banner + cisco.nxos.nxos_banner: + banner: motd + state: absent +- name: Configure banner from file + cisco.nxos.nxos_banner: + banner: motd + text: "{{ lookup('file', './config_partial/raw_banner.cfg') }}" + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - banner exec + - this is my exec banner + - that contains a multiline + - string +""" + +import re + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(module, command): + format = "text" + cmds = [{"command": command, "output": format}] + output = run_commands(module, cmds) + return output + + +def map_obj_to_commands(want, have, module): + commands = list() + state = module.params["state"] + platform_regex = "Nexus.*Switch" + + if state == "absent": + if have.get("text") and not ( + (have.get("text") == "User Access Verification") + or re.match(platform_regex, have.get("text")) + ): + commands.append("no banner %s" % module.params["banner"]) + + elif state == "present" and want.get("text") != have.get("text"): + banner_cmd = "banner %s @\n%s\n@" % ( + module.params["banner"], + want["text"], + ) + commands.append(banner_cmd) + + return commands + + +def map_config_to_obj(module): + command = "show banner %s" % module.params["banner"] + output = execute_show_command(module, command)[0] + + if "Invalid command" in output: + module.fail_json( + msg="banner: %s may not be supported on this platform. Possible values are : exec | motd" + % module.params["banner"], + ) + + if isinstance(output, dict): + output = list(output.values()) + if output != []: + output = output[0] + else: + output = "" + if isinstance(output, dict): + output = list(output.values()) + if output != []: + output = output[0] + else: + output = "" + else: + output = output.rstrip() + + obj = {"banner": module.params["banner"], "state": "absent"} + if output: + obj["text"] = output + obj["state"] = "present" + return obj + + +def map_params_to_obj(module): + text = module.params["text"] + return { + "banner": module.params["banner"], + "text": to_text(text) if text else None, + "state": module.params["state"], + } + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + banner=dict(required=True, choices=["exec", "motd"]), + text=dict(), + state=dict(default="present", choices=["present", "absent"]), + ) + + required_if = [("state", "present", ("text",))] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + want = map_params_to_obj(module) + have = map_config_to_obj(module) + commands = map_obj_to_commands(want, have, module) + result["commands"] = commands + + if commands: + if not module.check_mode: + msgs = load_config(module, commands, True) + if msgs: + for item in msgs: + if item: + if isinstance(item, dict): + err_str = item["clierror"] + else: + err_str = item + if "more than 40 lines" in err_str or "buffer overflowed" in err_str: + load_config(module, commands) + + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_global.py new file mode 100644 index 00000000..6ae1a88d --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_global.py @@ -0,0 +1,333 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bfd_global +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Bidirectional Forwarding Detection (BFD) global-level configuration +description: +- Manages Bidirectional Forwarding Detection (BFD) global-level configuration. +version_added: 1.0.0 +author: +- Chris Van Heuveln (@chrisvanheuveln) +notes: +- Tested against NXOSv 9.2(2) +- Unsupported for Cisco MDS +- BFD global will automatically enable 'feature bfd' if it is disabled. +- BFD global does not have a 'state' parameter. All of the BFD commands are unique + and are defined if 'feature bfd' is enabled. +options: + echo_interface: + description: + - Loopback interface used for echo frames. + - Valid values are loopback interface name or 'deleted'. + - Not supported on N5K/N6K + required: false + type: str + echo_rx_interval: + description: + - BFD Echo receive interval in milliseconds. + required: false + type: int + interval: + description: + - BFD interval timer values. + - Value must be a dict defining values for keys (tx, min_rx, and multiplier) + required: false + type: dict + slow_timer: + description: + - BFD slow rate timer in milliseconds. + required: false + type: int + startup_timer: + description: + - BFD delayed startup timer in seconds. + - Not supported on N5K/N6K/N7K + required: false + type: int + ipv4_echo_rx_interval: + description: + - BFD IPv4 session echo receive interval in milliseconds. + required: false + type: int + ipv4_interval: + description: + - BFD IPv4 interval timer values. + - Value must be a dict defining values for keys (tx, min_rx, and multiplier). + required: false + type: dict + ipv4_slow_timer: + description: + - BFD IPv4 slow rate timer in milliseconds. + required: false + type: int + ipv6_echo_rx_interval: + description: + - BFD IPv6 session echo receive interval in milliseconds. + required: false + type: int + ipv6_interval: + description: + - BFD IPv6 interval timer values. + - Value must be a dict defining values for keys (tx, min_rx, and multiplier). + required: false + type: dict + ipv6_slow_timer: + description: + - BFD IPv6 slow rate timer in milliseconds. + required: false + type: int + fabricpath_interval: + description: + - BFD fabricpath interval timer values. + - Value must be a dict defining values for keys (tx, min_rx, and multiplier). + required: false + type: dict + fabricpath_slow_timer: + description: + - BFD fabricpath slow rate timer in milliseconds. + required: false + type: int + fabricpath_vlan: + description: + - BFD fabricpath control vlan. + required: false + type: int +""" +EXAMPLES = """ +- cisco.nxos.nxos_bfd_global: + echo_interface: Ethernet1/2 + echo_rx_interval: 50 + interval: + tx: 50 + min_rx: 50 + multiplier: 4 +""" + +RETURN = """ +cmds: + description: commands sent to the device + returned: always + type: list + sample: ["bfd echo-interface loopback1", "bfd slow-timer 2000"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + NxosCmdRef, + load_config, +) + + +BFD_CMD_REF = """ +# The cmd_ref is a yaml formatted list of module commands. +# A leading underscore denotes a non-command variable; e.g. _template. +# BFD does not have convenient json data so this cmd_ref uses raw cli configs. +--- +_template: # _template holds common settings for all commands + # Enable feature bfd if disabled + feature: bfd + # Common get syntax for BFD commands + get_command: show run bfd all | incl '^(no )*bfd' + +echo_interface: + kind: str + getval: (no )*bfd echo-interface *(\\S+)*$ + setval: 'bfd echo-interface {0}' + default: ~ + +echo_rx_interval: + _exclude: ['N5K', 'N6K'] + kind: int + getval: bfd echo-rx-interval (\\d+)$ + setval: bfd echo-rx-interval {0} + default: 50 + N3K: + default: 250 + +interval: + kind: dict + getval: bfd interval (?P<tx>\\d+) min_rx (?P<min_rx>\\d+) multiplier (?P<multiplier>\\d+) + setval: bfd interval {tx} min_rx {min_rx} multiplier {multiplier} + default: &def_interval + tx: 50 + min_rx: 50 + multiplier: 3 + N3K: + default: &n3k_def_interval + tx: 250 + min_rx: 250 + multiplier: 3 + +slow_timer: + kind: int + getval: bfd slow-timer (\\d+)$ + setval: bfd slow-timer {0} + default: 2000 + +startup_timer: + _exclude: ['N5K', 'N6K', 'N7K'] + kind: int + getval: bfd startup-timer (\\d+)$ + setval: bfd startup-timer {0} + default: 5 + +# IPv4/IPv6 specific commands +ipv4_echo_rx_interval: + _exclude: ['N5K', 'N6K'] + kind: int + getval: bfd ipv4 echo-rx-interval (\\d+)$ + setval: bfd ipv4 echo-rx-interval {0} + default: 50 + N3K: + default: 250 + +ipv4_interval: + _exclude: ['N5K', 'N6K'] + kind: dict + getval: bfd ipv4 interval (?P<tx>\\d+) min_rx (?P<min_rx>\\d+) multiplier (?P<multiplier>\\d+) + setval: bfd ipv4 interval {tx} min_rx {min_rx} multiplier {multiplier} + default: *def_interval + N3K: + default: *n3k_def_interval + +ipv4_slow_timer: + _exclude: ['N5K', 'N6K'] + kind: int + getval: bfd ipv4 slow-timer (\\d+)$ + setval: bfd ipv4 slow-timer {0} + default: 2000 + +ipv6_echo_rx_interval: + _exclude: ['N35', 'N5K', 'N6K'] + kind: int + getval: bfd ipv6 echo-rx-interval (\\d+)$ + setval: bfd ipv6 echo-rx-interval {0} + default: 50 + N3K: + default: 250 + +ipv6_interval: + _exclude: ['N35', 'N5K', 'N6K'] + kind: dict + getval: bfd ipv6 interval (?P<tx>\\d+) min_rx (?P<min_rx>\\d+) multiplier (?P<multiplier>\\d+) + setval: bfd ipv6 interval {tx} min_rx {min_rx} multiplier {multiplier} + default: *def_interval + N3K: + default: *n3k_def_interval + +ipv6_slow_timer: + _exclude: ['N35', 'N5K', 'N6K'] + kind: int + getval: bfd ipv6 slow-timer (\\d+)$ + setval: bfd ipv6 slow-timer {0} + default: 2000 + +# Fabricpath Commands +fabricpath_interval: + _exclude: ['N35', 'N3K', 'N9K'] + kind: dict + getval: bfd fabricpath interval (?P<tx>\\d+) min_rx (?P<min_rx>\\d+) multiplier (?P<multiplier>\\d+) + setval: bfd fabricpath interval {tx} min_rx {min_rx} multiplier {multiplier} + default: *def_interval + +fabricpath_slow_timer: + _exclude: ['N35', 'N3K', 'N9K'] + kind: int + getval: bfd fabricpath slow-timer (\\d+)$ + setval: bfd fabricpath slow-timer {0} + default: 2000 + +fabricpath_vlan: + _exclude: ['N35', 'N3K', 'N9K'] + kind: int + getval: bfd fabricpath vlan (\\d+)$ + setval: bfd fabricpath vlan {0} + default: 1 +""" + + +def reorder_cmds(cmds): + """ + There is a bug in some image versions where bfd echo-interface and + bfd echo-rx-interval need to be applied last for them to nvgen properly. + """ + regex1 = re.compile(r"^bfd echo-interface") + regex2 = re.compile(r"^bfd echo-rx-interval") + filtered_cmds = [i for i in cmds if not regex1.match(i)] + filtered_cmds = [i for i in filtered_cmds if not regex2.match(i)] + echo_int_cmd = [i for i in cmds if regex1.match(i)] + echo_rx_cmd = [i for i in cmds if regex2.match(i)] + filtered_cmds.extend(echo_int_cmd) + filtered_cmds.extend(echo_rx_cmd) + + return filtered_cmds + + +def main(): + argument_spec = dict( + echo_interface=dict(required=False, type="str"), + echo_rx_interval=dict(required=False, type="int"), + interval=dict(required=False, type="dict"), + slow_timer=dict(required=False, type="int"), + startup_timer=dict(required=False, type="int"), + ipv4_echo_rx_interval=dict(required=False, type="int"), + ipv4_interval=dict(required=False, type="dict"), + ipv4_slow_timer=dict(required=False, type="int"), + ipv6_echo_rx_interval=dict(required=False, type="int"), + ipv6_interval=dict(required=False, type="dict"), + ipv6_slow_timer=dict(required=False, type="int"), + fabricpath_interval=dict(required=False, type="dict"), + fabricpath_slow_timer=dict(required=False, type="int"), + fabricpath_vlan=dict(required=False, type="int"), + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + warnings = list() + + cmd_ref = NxosCmdRef(module, BFD_CMD_REF) + cmd_ref.get_existing() + cmd_ref.get_playvals() + cmds = reorder_cmds(cmd_ref.get_proposed()) + + result = { + "changed": False, + "commands": cmds, + "warnings": warnings, + "check_mode": module.check_mode, + } + if cmds: + result["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_interfaces.py new file mode 100644 index 00000000..1790f8e0 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bfd_interfaces.py @@ -0,0 +1,302 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_bfd_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bfd_interfaces +short_description: BFD interfaces resource module +description: Manages attributes of Bidirectional Forwarding Detection (BFD) on the + interface. +version_added: 1.0.0 +author: Chris Van Heuveln (@chrisvanheuveln) +notes: +- Tested against NX-OS 7.0(3)I5(1). +- Unsupported for Cisco MDS +- Feature bfd should be enabled for this module. +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^interface|^feature + bfd'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: The provided configuration + type: list + elements: dict + suboptions: + name: + type: str + description: The name of the interface. + bfd: + type: str + description: + - Enable/Disable Bidirectional Forwarding Detection (BFD) on the interface. + choices: + - enable + - disable + echo: + type: str + description: + - Enable/Disable BFD Echo functionality on the interface. + choices: + - enable + - disable + state: + description: + - The state of the configuration after module completion + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using deleted + +- name: Configure interfaces + cisco.nxos.nxos_bfd_interfaces: + state: deleted + + +# Using merged + +- name: Configure interfaces + cisco.nxos.nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + state: merged + + +# Using overridden + +- name: Configure interfaces + cisco.nxos.nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + state: overridden + + +# Using replaced + +- name: Configure interfaces + cisco.nxos.nxos_bfd_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + echo: enable + - name: Ethernet1/2 + bfd: disable + echo: disable + state: replaced + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_bfd_interfaces: + config: + - name: Ethernet1/800 + bfd: enable + echo: enable + - name: Ethernet1/801 + bfd: disable + echo: disable + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/800" +# - "bfd" +# - "bfd echo" +# - "interface Ethernet1/801" +# - "no bfd" +# - "no bfd echo" + +# Using parsed + +# parsed.cfg +# ------------ + +# feature bfd +# interface Ethernet1/800 +# no switchport +# no bfd +# no bfd echo +# interface Ethernet1/801 +# no switchport +# no bfd +# interface Ethernet1/802 +# no switchport +# no bfd echo +# interface mgmt0 +# ip address dhcp +# vrf member management + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_bfd_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - bfd: disable +# echo: disable +# name: Ethernet1/800 +# - bfd: disable +# echo: enable +# name: Ethernet1/801 +# - bfd: enable +# echo: disable +# name: Ethernet1/802 +# - bfd: enable +# echo: enable +# name: mgmt0 + +# Using gathered + +# Existing device config state +# ------------------------------- + +# feature bfd +# interface Ethernet1/1 +# no switchport +# no bfd +# interface Ethernet1/2 +# no switchport +# no bfd echo +# interface mgmt0 +# ip address dhcp +# vrf member management + +- name: Gather bfd_interfaces facts from the device using nxos_bfd_interfaces + cisco.nxos.nxos_bfd_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# - name: Ethernet1/1 +# bfd: disable +# echo: enable +# - name: Ethernet1/3 +# echo: disable +# bfd: enable +# - name: mgmt0 +# bfd: enable +# echo: enable +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/1', 'no bfd', 'no bfd echo'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.bfd_interfaces.bfd_interfaces import ( + Bfd_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.bfd_interfaces.bfd_interfaces import ( + Bfd_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Bfd_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Bfd_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp.py new file mode 100644 index 00000000..2c56f412 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp.py @@ -0,0 +1,761 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bgp +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2023-01-27) Manages BGP configuration. +description: +- Manages BGP configurations on NX-OS switches. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +deprecated: + alternative: nxos_bgp_global + why: Updated module released with more functionality. + removed_at_date: '2023-01-27' +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) removes the whole BGP ASN configuration when C(vrf=default) or the + whole VRF instance within the BGP process when using a different VRF. +- Default when supported restores params default value. +- Configuring global params is only permitted if C(vrf=default). +options: + asn: + description: + - BGP autonomous system number. Valid values are String, Integer in ASPLAIN or + ASDOT notation. + required: true + type: str + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing the global BGP. + default: 'default' + type: str + bestpath_always_compare_med: + description: + - Enable/Disable MED comparison on paths from different autonomous systems. + type: bool + bestpath_aspath_multipath_relax: + description: + - Enable/Disable load sharing across the providers with different (but equal-length) + AS paths. + type: bool + bestpath_compare_routerid: + description: + - Enable/Disable comparison of router IDs for identical eBGP paths. + type: bool + bestpath_compare_neighborid: + description: + - Enable/Disable neighborid. Use this when more paths available than max path + config. + type: bool + bestpath_cost_community_ignore: + description: + - Enable/Disable Ignores the cost community for BGP best-path calculations. + type: bool + bestpath_med_confed: + description: + - Enable/Disable enforcement of bestpath to do a MED comparison only between paths + originated within a confederation. + type: bool + bestpath_med_missing_as_worst: + description: + - Enable/Disable assigns the value of infinity to received routes that do not + carry the MED attribute, making these routes the least desirable. + type: bool + bestpath_med_non_deterministic: + description: + - Enable/Disable deterministic selection of the best MED pat from among the paths + from the same autonomous system. + type: bool + cluster_id: + description: + - Route Reflector Cluster-ID. + type: str + confederation_id: + description: + - Routing domain confederation AS. + type: str + confederation_peers: + description: + - AS confederation parameters. + type: list + elements: str + disable_policy_batching: + description: + - Enable/Disable the batching evaluation of prefix advertisement to all peers. + type: bool + disable_policy_batching_ipv4_prefix_list: + description: + - Enable/Disable the batching evaluation of prefix advertisements to all peers + with prefix list. + type: str + disable_policy_batching_ipv6_prefix_list: + description: + - Enable/Disable the batching evaluation of prefix advertisements to all peers + with prefix list. + type: str + enforce_first_as: + description: + - Enable/Disable enforces the neighbor autonomous system to be the first AS number + listed in the AS path attribute for eBGP. On NX-OS, this property is only supported + in the global BGP context. + type: bool + event_history_cli: + description: + - Enable/Disable cli event history buffer. + choices: + - size_small + - size_medium + - size_large + - size_disable + - default + - 'true' + - 'false' + type: str + event_history_detail: + description: + - Enable/Disable detail event history buffer. + choices: + - size_small + - size_medium + - size_large + - size_disable + - default + - 'true' + - 'false' + type: str + event_history_events: + description: + - Enable/Disable event history buffer. + choices: + - size_small + - size_medium + - size_large + - size_disable + - default + - 'true' + - 'false' + type: str + event_history_periodic: + description: + - Enable/Disable periodic event history buffer. + choices: + - size_small + - size_medium + - size_large + - size_disable + - default + - 'true' + - 'false' + type: str + fast_external_fallover: + description: + - Enable/Disable immediately reset the session if the link to a directly connected + BGP peer goes down. Only supported in the global BGP context. + type: bool + flush_routes: + description: + - Enable/Disable flush routes in RIB upon controlled restart. On NX-OS, this property + is only supported in the global BGP context. + type: bool + graceful_restart: + description: + - Enable/Disable graceful restart. + type: bool + graceful_restart_helper: + description: + - Enable/Disable graceful restart helper mode. + type: bool + graceful_restart_timers_restart: + description: + - Set maximum time for a restart sent to the BGP peer. + type: str + graceful_restart_timers_stalepath_time: + description: + - Set maximum time that BGP keeps the stale routes from the restarting BGP peer. + type: str + isolate: + description: + - Enable/Disable isolate this router from BGP perspective. + type: bool + local_as: + description: + - Local AS number to be used within a VRF instance. + type: str + log_neighbor_changes: + description: + - Enable/Disable message logging for neighbor up/down event. + type: bool + maxas_limit: + description: + - Specify Maximum number of AS numbers allowed in the AS-path attribute. Valid + values are between 1 and 512. + type: str + neighbor_down_fib_accelerate: + description: + - Enable/Disable handle BGP neighbor down event, due to various reasons. + type: bool + reconnect_interval: + description: + - The BGP reconnection interval for dropped sessions. Valid values are between + 1 and 60. + type: str + router_id: + description: + - Router Identifier (ID) of the BGP router VRF instance. + type: str + shutdown: + description: + - Administratively shutdown the BGP protocol. + type: bool + suppress_fib_pending: + description: + - Enable/Disable advertise only routes programmed in hardware to peers. + type: bool + timer_bestpath_limit: + description: + - Specify timeout for the first best path after a restart, in seconds. + type: str + timer_bgp_hold: + description: + - Set BGP hold timer. + type: str + timer_bgp_keepalive: + description: + - Set BGP keepalive timer. + type: str + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str +""" + + +EXAMPLES = """ +- name: Configure a simple ASN + cisco.nxos.nxos_bgp: + asn: 65535 + vrf: test + router_id: 192.0.2.1 + state: present +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf test", "router-id 192.0.2.1"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +BOOL_PARAMS = [ + "bestpath_always_compare_med", + "bestpath_aspath_multipath_relax", + "bestpath_compare_neighborid", + "bestpath_compare_routerid", + "bestpath_cost_community_ignore", + "bestpath_med_confed", + "bestpath_med_missing_as_worst", + "bestpath_med_non_deterministic", + "disable_policy_batching", + "enforce_first_as", + "fast_external_fallover", + "flush_routes", + "graceful_restart", + "graceful_restart_helper", + "isolate", + "log_neighbor_changes", + "neighbor_down_fib_accelerate", + "shutdown", + "suppress_fib_pending", +] +GLOBAL_PARAMS = [ + "disable_policy_batching", + "disable_policy_batching_ipv4_prefix_list", + "disable_policy_batching_ipv6_prefix_list", + "enforce_first_as", + "event_history_cli", + "event_history_detail", + "event_history_events", + "event_history_periodic", + "fast_external_fallover", + "flush_routes", + "isolate", + "suppress_fib_pending", + "shutdown", +] +PARAM_TO_DEFAULT_KEYMAP = { + "timer_bgp_keepalive": "60", + "timer_bgp_hold": "180", + "timer_bestpath_limit": "300", + "graceful_restart": True, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", + "reconnect_interval": "60", + "suppress_fib_pending": True, + "fast_external_fallover": True, + "enforce_first_as": True, + "event_history_cli": True, + "event_history_detail": False, + "event_history_events": True, + "event_history_periodic": True, + "maxas_limit": "", + "router_id": "", + "cluster_id": "", + "disable_policy_batching_ipv4_prefix_list": "", + "disable_policy_batching_ipv6_prefix_list": "", + "local_as": "", + "confederation_id": "", +} +PARAM_TO_COMMAND_KEYMAP = { + "asn": "router bgp", + "bestpath_always_compare_med": "bestpath always-compare-med", + "bestpath_aspath_multipath_relax": "bestpath as-path multipath-relax", + "bestpath_compare_neighborid": "bestpath compare-neighborid", + "bestpath_compare_routerid": "bestpath compare-routerid", + "bestpath_cost_community_ignore": "bestpath cost-community ignore", + "bestpath_med_confed": "bestpath med confed", + "bestpath_med_missing_as_worst": "bestpath med missing-as-worst", + "bestpath_med_non_deterministic": "bestpath med non-deterministic", + "cluster_id": "cluster-id", + "confederation_id": "confederation identifier", + "confederation_peers": "confederation peers", + "disable_policy_batching": "disable-policy-batching", + "disable_policy_batching_ipv4_prefix_list": "disable-policy-batching ipv4 prefix-list", + "disable_policy_batching_ipv6_prefix_list": "disable-policy-batching ipv6 prefix-list", + "enforce_first_as": "enforce-first-as", + "event_history_cli": "event-history cli", + "event_history_detail": "event-history detail", + "event_history_events": "event-history events", + "event_history_periodic": "event-history periodic", + "fast_external_fallover": "fast-external-fallover", + "flush_routes": "flush-routes", + "graceful_restart": "graceful-restart", + "graceful_restart_helper": "graceful-restart-helper", + "graceful_restart_timers_restart": "graceful-restart restart-time", + "graceful_restart_timers_stalepath_time": "graceful-restart stalepath-time", + "isolate": "isolate", + "local_as": "local-as", + "log_neighbor_changes": "log-neighbor-changes", + "maxas_limit": "maxas-limit", + "neighbor_down_fib_accelerate": "neighbor-down fib-accelerate", + "reconnect_interval": "reconnect-interval", + "router_id": "router-id", + "shutdown": "shutdown", + "suppress_fib_pending": "suppress-fib-pending", + "timer_bestpath_limit": "timers bestpath-limit", + "timer_bgp_hold": "timers bgp", + "timer_bgp_keepalive": "timers bgp", + "vrf": "vrf", +} + + +def get_value(arg, config): + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + + if command.split()[0] == "event-history": + has_size = re.search(r"^\s+{0} size\s(?P<value>.*)$".format(command), config, re.M) + + if command == "event-history detail": + value = False + else: + value = "size_small" + + if has_size: + value = "size_%s" % has_size.group("value") + + elif arg in ["enforce_first_as", "fast_external_fallover"]: + no_command_re = re.compile(r"no\s+{0}\s*".format(command), re.M) + value = True + + if no_command_re.search(config): + value = False + + elif arg in BOOL_PARAMS: + has_command = re.search(r"^\s+{0}\s*$".format(command), config, re.M) + value = False + + if has_command: + value = True + else: + command_val_re = re.compile(r"(?:{0}\s)(?P<value>.*)".format(command), re.M) + value = "" + + has_command = command_val_re.search(config) + if has_command: + found_value = has_command.group("value") + + if arg == "confederation_peers": + value = found_value.split() + elif arg == "timer_bgp_keepalive": + value = found_value.split()[0] + elif arg == "timer_bgp_hold": + split_values = found_value.split() + if len(split_values) == 2: + value = split_values[1] + elif found_value: + value = found_value + + return value + + +def get_existing(module, args, warnings): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module, flags=["bgp all"])) + + asn_re = re.compile(r".*router\sbgp\s(?P<existing_asn>\d+(\.\d+)?).*", re.S) + asn_match = asn_re.match(str(netcfg)) + + if asn_match: + existing_asn = asn_match.group("existing_asn") + bgp_parent = "router bgp {0}".format(existing_asn) + + if module.params["vrf"] != "default": + parents = [bgp_parent, "vrf {0}".format(module.params["vrf"])] + else: + parents = [bgp_parent] + + config = netcfg.get_section(parents) + if config: + for arg in args: + if arg != "asn" and (module.params["vrf"] == "default" or arg not in GLOBAL_PARAMS): + existing[arg] = get_value(arg, config) + + existing["asn"] = existing_asn + if module.params["vrf"] == "default": + existing["vrf"] = "default" + + if not existing and module.params["vrf"] != "default" and module.params["state"] == "present": + msg = "VRF {0} doesn't exist.".format(module.params["vrf"]) + warnings.append(msg) + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key in table: + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = table.get(key) + + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.items(): + if value is True: + commands.append(key) + elif value is False: + commands.append("no {0}".format(key)) + elif value == "default": + default_value = PARAM_TO_DEFAULT_KEYMAP.get(key) + existing_value = existing_commands.get(key) + + if default_value: + commands.append("{0} {1}".format(key, default_value)) + elif existing_value: + if key == "confederation peers": + existing_value = " ".join(existing_value) + commands.append("no {0} {1}".format(key, existing_value)) + elif not value: + existing_value = existing_commands.get(key) + if existing_value: + commands.append("no {0} {1}".format(key, existing_value)) + elif key == "confederation peers": + commands.append("{0} {1}".format(key, value)) + elif key.startswith("timers bgp"): + command = "timers bgp {0} {1}".format( + proposed["timer_bgp_keepalive"], + proposed["timer_bgp_hold"], + ) + if command not in commands: + commands.append(command) + else: + if value.startswith("size"): + value = value.replace("_", " ") + command = "{0} {1}".format(key, value) + commands.append(command) + + parents = [] + if commands: + commands = fix_commands(commands) + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + elif proposed: + if module.params["vrf"] != "default": + commands.append("vrf {0}".format(module.params["vrf"])) + parents = ["router bgp {0}".format(module.params["asn"])] + else: + commands.append("router bgp {0}".format(module.params["asn"])) + + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, candidate): + commands = [] + parents = [] + if module.params["vrf"] == "default": + commands.append("no router bgp {0}".format(module.params["asn"])) + elif existing.get("vrf") == module.params["vrf"]: + commands.append("no vrf {0}".format(module.params["vrf"])) + parents = ["router bgp {0}".format(module.params["asn"])] + + candidate.add(commands, parents=parents) + + +def fix_commands(commands): + local_as_command = "" + confederation_id_command = "" + confederation_peers_command = "" + + for command in commands: + if "local-as" in command: + local_as_command = command + elif "confederation identifier" in command: + confederation_id_command = command + elif "confederation peers" in command: + confederation_peers_command = command + + if local_as_command and confederation_id_command: + if "no" in confederation_id_command: + commands.pop(commands.index(local_as_command)) + commands.pop(commands.index(confederation_id_command)) + commands.append(confederation_id_command) + commands.append(local_as_command) + else: + commands.pop(commands.index(local_as_command)) + commands.pop(commands.index(confederation_id_command)) + commands.append(local_as_command) + commands.append(confederation_id_command) + + if confederation_peers_command and confederation_id_command: + if local_as_command: + if "no" in local_as_command: + commands.pop(commands.index(local_as_command)) + commands.pop(commands.index(confederation_id_command)) + commands.pop(commands.index(confederation_peers_command)) + commands.append(confederation_id_command) + commands.append(confederation_peers_command) + commands.append(local_as_command) + else: + commands.pop(commands.index(local_as_command)) + commands.pop(commands.index(confederation_id_command)) + commands.pop(commands.index(confederation_peers_command)) + commands.append(local_as_command) + commands.append(confederation_id_command) + commands.append(confederation_peers_command) + else: + commands.pop(commands.index(confederation_peers_command)) + commands.pop(commands.index(confederation_id_command)) + commands.append(confederation_id_command) + commands.append(confederation_peers_command) + + return commands + + +def main(): + argument_spec = dict( + asn=dict(required=True, type="str"), + vrf=dict(required=False, type="str", default="default"), + bestpath_always_compare_med=dict(required=False, type="bool"), + bestpath_aspath_multipath_relax=dict(required=False, type="bool"), + bestpath_compare_neighborid=dict(required=False, type="bool"), + bestpath_compare_routerid=dict(required=False, type="bool"), + bestpath_cost_community_ignore=dict(required=False, type="bool"), + bestpath_med_confed=dict(required=False, type="bool"), + bestpath_med_missing_as_worst=dict(required=False, type="bool"), + bestpath_med_non_deterministic=dict(required=False, type="bool"), + cluster_id=dict(required=False, type="str"), + confederation_id=dict(required=False, type="str"), + confederation_peers=dict(required=False, type="list", elements="str"), + disable_policy_batching=dict(required=False, type="bool"), + disable_policy_batching_ipv4_prefix_list=dict(required=False, type="str"), + disable_policy_batching_ipv6_prefix_list=dict(required=False, type="str"), + enforce_first_as=dict(required=False, type="bool"), + event_history_cli=dict( + required=False, + choices=[ + "true", + "false", + "default", + "size_small", + "size_medium", + "size_large", + "size_disable", + ], + ), + event_history_detail=dict( + required=False, + choices=[ + "true", + "false", + "default", + "size_small", + "size_medium", + "size_large", + "size_disable", + ], + ), + event_history_events=dict( + required=False, + choices=[ + "true", + "false", + "default", + "size_small", + "size_medium", + "size_large", + "size_disable", + ], + ), + event_history_periodic=dict( + required=False, + choices=[ + "true", + "false", + "default", + "size_small", + "size_medium", + "size_large", + "size_disable", + ], + ), + fast_external_fallover=dict(required=False, type="bool"), + flush_routes=dict(required=False, type="bool"), + graceful_restart=dict(required=False, type="bool"), + graceful_restart_helper=dict(required=False, type="bool"), + graceful_restart_timers_restart=dict(required=False, type="str"), + graceful_restart_timers_stalepath_time=dict(required=False, type="str"), + isolate=dict(required=False, type="bool"), + local_as=dict(required=False, type="str"), + log_neighbor_changes=dict(required=False, type="bool"), + maxas_limit=dict(required=False, type="str"), + neighbor_down_fib_accelerate=dict(required=False, type="bool"), + reconnect_interval=dict(required=False, type="str"), + router_id=dict(required=False, type="str"), + shutdown=dict(required=False, type="bool"), + suppress_fib_pending=dict(required=False, type="bool"), + timer_bestpath_limit=dict(required=False, type="str"), + timer_bgp_hold=dict(required=False, type="str"), + timer_bgp_keepalive=dict(required=False, type="str"), + state=dict(choices=["present", "absent"], default="present", required=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=[["timer_bgp_hold", "timer_bgp_keepalive"]], + supports_check_mode=True, + ) + + warnings = list() + result = dict(changed=False, warnings=warnings) + + state = module.params["state"] + + if module.params["vrf"] != "default": + for param in GLOBAL_PARAMS: + if module.params[param]: + module.fail_json( + msg='Global params can be modified only under "default" VRF.', + vrf=module.params["vrf"], + global_param=param, + ) + + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args, warnings) + + if existing.get("asn") and state == "present": + if existing.get("asn") != module.params["asn"]: + module.fail_json( + msg="Another BGP ASN already exists.", + proposed_asn=module.params["asn"], + existing_asn=existing.get("asn"), + ) + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + proposed = {} + for key, value in proposed_args.items(): + if key not in ["asn", "vrf"]: + if str(value).lower() == "default": + value = PARAM_TO_DEFAULT_KEYMAP.get(key, "default") + if key == "confederation_peers": + if value[0] == "default": + if existing.get(key): + proposed[key] = "default" + else: + v = set([int(i) for i in value]) + ex = set([int(i) for i in existing.get(key)]) + if v != ex: + proposed[key] = " ".join(str(s) for s in v) + else: + if existing.get(key) != value: + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + if state == "present": + state_present(module, existing, proposed, candidate) + elif existing.get("asn") == module.params["asn"]: + state_absent(module, existing, candidate) + + if candidate: + candidate = candidate.items_text() + if not module.check_mode: + load_config(module, candidate) + result["changed"] = True + result["commands"] = candidate + else: + result["commands"] = [] + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_address_family.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_address_family.py new file mode 100644 index 00000000..c838d25f --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_address_family.py @@ -0,0 +1,1031 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_bgp_address_family +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_bgp_address_family +short_description: BGP Address Family resource module. +description: +- This module manages BGP Address Family configuration on devices running Cisco NX-OS. +version_added: 2.0.0 +notes: +- Tested against NX-OS 9.3.6. +- Unsupported for Cisco MDS +- For managing BGP neighbor address family configurations please use + the M(cisco.nxos.nxos_bgp_neighbor_address_family) module. +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^router bgp'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of BGP process configuration. + type: dict + suboptions: + as_number: + description: Autonomous System Number of the router. + type: str + address_family: + description: Address Family related configurations. + type: list + elements: dict + suboptions: + afi: + description: Address Family indicator. + type: str + choices: ["ipv4", "ipv6", "link-state", "vpnv4", "vpnv6", "l2vpn"] + required: True + safi: + description: Sub Address Family indicator. + type: str + choices: ["unicast", "multicast", "mvpn", "evpn"] + additional_paths: + description: Additional paths configuration. + type: dict + suboptions: + install_backup: + description: Install backup path. + type: bool + receive: + description: Additional paths Receive capability. + type: bool + selection: + description: Additional paths selection + type: dict + suboptions: + route_map: + description: Route-map for additional paths selection + type: str + send: + description: Additional paths Send capability + type: bool + advertise_pip: + description: Advertise physical ip for type-5 route. + type: bool + advertise_l2vpn_evpn: + description: Enable advertising EVPN routes. + type: bool + advertise_system_mac: + description: Advertise extra EVPN RT-2 with system MAC. + type: bool + allow_vni_in_ethertag: + description: Allow VNI in Ethernet Tag field in EVPN route. + type: bool + aggregate_address: + description: Configure BGP aggregate prefixes + type: list + elements: dict + suboptions: + prefix: + description: Aggregate prefix. + type: str + advertise_map: + description: Select attribute information from specific routes. + type: str + as_set: + description: Generate AS-SET information. + type: bool + attribute_map: + description: Set attribute information of aggregate. + type: str + summary_only: + description: Do not advertise more specifics. + type: bool + suppress_map: + description: Conditionally filter more specific routes. + type: str + client_to_client: + description: Configure client-to-client route reflection. + type: dict + suboptions: + no_reflection: + description: Reflection of routes permitted. + type: bool + dampen_igp_metric: + description: Dampen IGP metric-related changes. + type: int + dampening: + description: Configure route flap dampening. + type: dict + suboptions: + set: + description: Set route flap dampening. + type: bool + decay_half_life: + description: Decay half life. + type: int + start_reuse_route: + description: Value to start reusing a route. + type: int + start_suppress_route: + description: Value to start suppressing a route. + type: int + max_suppress_time: + description: Maximum suppress time for stable route. + type: int + route_map: + description: Apply route-map to specify dampening criteria. + type: str + default_information: + description: Control distribution of default information. + type: dict + suboptions: + originate: + description: Distribute a default route. + type: bool + default_metric: + description: Set metric of redistributed routes. + type: int + distance: + description: Configure administrative distance. + type: dict + suboptions: + ebgp_routes: + description: Distance for EBGP routes. + type: int + ibgp_routes: + description: Distance for IBGP routes. + type: int + local_routes: + description: Distance for local routes. + type: int + export_gateway_ip: + description: Export Gateway IP to Type-5 EVPN routes for VRF + type: bool + inject_map: + description: Routemap which specifies prefixes to inject. + type: list + elements: dict + suboptions: + route_map: + description: Route-map name. + type: str + exist_map: + description: Routemap which specifies exist condition. + type: str + copy_attributes: + description: Copy attributes from aggregate. + type: bool + maximum_paths: + description: Forward packets over multipath paths. + type: dict + suboptions: + parallel_paths: + description: Number of parallel paths. + type: int + ibgp: + description: Configure multipath for IBGP paths. + type: dict + suboptions: + parallel_paths: + description: Number of parallel paths. + type: int + eibgp: + description: Configure multipath for both EBGP and IBGP paths. + type: dict + suboptions: + parallel_paths: + description: Number of parallel paths. + type: int + local: + description: Configure multipath for local paths. + type: dict + suboptions: + parallel_paths: + description: Number of parallel paths. + type: int + mixed: + description: Configure multipath for local and remote paths. + type: dict + suboptions: + parallel_paths: + description: Number of parallel paths. + type: int + networks: + description: Configure an IP prefix to advertise. + type: list + elements: dict + suboptions: + prefix: + description: IP prefix in CIDR format. + type: str + route_map: + description: Route-map name. + type: str + nexthop: + description: Nexthop tracking. + type: dict + suboptions: + route_map: + description: Route-map name. + type: str + trigger_delay: + description: Set the delay to trigger nexthop tracking. + type: dict + suboptions: + critical_delay: + description: + - Nexthop changes affecting reachability. + - Delay value (miliseconds). + type: int + non_critical_delay: + description: + - Other nexthop changes. + - Delay value (miliseconds). + type: int + redistribute: + description: Configure redistribution. + type: list + elements: dict + suboptions: + protocol: + description: + - The name of the protocol. + type: str + choices: ["am", "direct", "eigrp", "isis", "lisp", "ospf", "ospfv3", "rip", "static", "hmm"] + required: true + id: + description: + - The identifier for the protocol specified. + type: str + route_map: + description: + - The route map policy to constrain redistribution. + type: str + required: true + retain: + description: Retain the routes based on Target VPN Extended Communities. + type: dict + suboptions: + route_target: + description: Specify Target VPN Extended Communities + type: dict + suboptions: + retain_all: + description: All the routes regardless of Target-VPN community + type: bool + route_map: + description: Apply route-map to filter routes. + type: str + suppress_inactive: + description: Advertise only active routes to peers. + type: bool + table_map: + description: + - Policy for filtering/modifying OSPF routes before sending them to RIB. + type: dict + suboptions: + name: + description: + - The Route Map name. + type: str + required: true + filter: + description: + - Block the OSPF routes from being sent to RIB. + type: bool + timers: + description: Configure bgp related timers. + type: dict + suboptions: + bestpath_defer: + description: Configure bestpath defer timer value for batch prefix processing. + type: dict + suboptions: + defer_time: + description: Bestpath defer time (mseconds). + type: int + maximum_defer_time: + description: Maximum bestpath defer time (mseconds). + type: int + wait_igp_convergence: + description: Delay initial bestpath until redistributed IGPs have converged. + type: bool + vrf: + description: Virtual Router Context. + type: str + state: + description: + - The state the configuration should be left in. + - State I(deleted) only removes BGP attributes that this modules + manages and does not negate the BGP process completely. + - Refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# Nexus9000v# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_bgp_address_family: + config: + as_number: 65536 + address_family: + - afi: ipv4 + safi: multicast + networks: + - prefix: 192.0.2.32/27 + - prefix: 192.0.2.64/27 + route_map: rmap1 + nexthop: + route_map: rmap2 + trigger_delay: + critical_delay: 120 + non_critical_delay: 180 + - afi: ipv4 + safi: unicast + vrf: site-1 + default_information: + originate: True + aggregate_address: + - prefix: 203.0.113.0/24 + as_set: True + summary_only: True + - afi: ipv6 + safi: multicast + vrf: site-1 + redistribute: + - protocol: ospfv3 + id: 100 + route_map: rmap-ospf-1 + - protocol: eigrp + id: 101 + route_map: rmap-eigrp-1 + +# Task output +# ------------- +# before: {} +# +# commands: +# - router bgp 65536 +# - address-family ipv4 multicast +# - nexthop route-map rmap2 +# - nexthop trigger-delay critical 120 non-critical 180 +# - network 192.0.2.32/27 +# - network 192.0.2.64/27 route-map rmap1 +# - vrf site-1 +# - address-family ipv4 unicast +# - default-information originate +# - aggregate-address 203.0.113.0/24 as-set summary-only +# - address-family ipv6 multicast +# - redistribute ospfv3 100 route-map rmap-ospf-1 +# - redistribute eigrp 101 route-map rmap-eigrp-1 +# +# after: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 +# + +# Using replaced + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +- name: Replace configuration of specified AFs + cisco.nxos.nxos_bgp_address_family: + config: + as_number: 65536 + address_family: + - afi: ipv4 + safi: multicast + networks: + - prefix: 192.0.2.64/27 + route_map: rmap1 + nexthop: + route_map: rmap2 + trigger_delay: + critical_delay: 120 + non_critical_delay: 180 + aggregate_address: + - prefix: 203.0.113.0/24 + as_set: True + summary_only: True + - afi: ipv4 + safi: unicast + vrf: site-1 + state: replaced + +# Task output +# ------------- +# before: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 +# +# commands: +# - router bgp 65536 +# - address-family ipv4 multicast +# - no network 192.0.2.32/27 +# - aggregate-address 203.0.113.0/24 as-set summary-only +# - vrf site-1 +# - address-family ipv4 unicast +# - no default-information originate +# - no aggregate-address 203.0.113.0/24 as-set summary-only +# +# after: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - protocol: ospfv3 +# id: "100" +# route_map: rmap-ospf-1 +# - protocol: eigrp +# id: "101" +# route_map: rmap-eigrp-1 + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.64/27 route-map rmap1 +# aggregate-address 203.0.113.0/24 as-set summary-only +# vrf site-1 +# address-family ipv4 unicast +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +# Using overridden + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +- name: Override all BGP AF configuration with provided configuration + cisco.nxos.nxos_bgp_address_family: &overridden + config: + as_number: 65536 + address_family: + - afi: ipv4 + safi: multicast + networks: + - prefix: 192.0.2.64/27 + route_map: rmap1 + aggregate_address: + - prefix: 203.0.113.0/24 + as_set: True + summary_only: True + - afi: ipv4 + safi: unicast + vrf: site-1 + state: overridden + +# Task output +# ------------- +# before: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 +# +# commands: +# - router bgp 65536 +# - vrf site-1 +# - no address-family ipv6 multicast +# - exit +# - address-family ipv4 multicast +# - no nexthop route-map rmap2 +# - no nexthop trigger-delay critical 120 non-critical 180 +# - aggregate-address 203.0.113.0/24 as-set summary-only +# - no network 192.0.2.32/27 +# - vrf site-1 +# - address-family ipv4 unicast +# - no default-information originate +# - no aggregate-address 203.0.113.0/24 as-set summary-only +# +# after: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv4 +# safi: unicast +# vrf: site-1 + +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# network 192.0.2.64/27 route-map rmap1 +# aggregate-address 203.0.113.0/24 as-set summary-only +# vrf site-1 +# address-family ipv4 unicast +# + +# Using deleted to remove specified AFs + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +- name: Delete specified BGP AFs + cisco.nxos.nxos_bgp_address_family: + config: + as_number: 65536 + address_family: + - afi: ipv4 + safi: multicast + - vrf: site-1 + afi: ipv6 + safi: multicast + state: deleted + +# Task output +# ------------- +# before: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 +# +# commands: +# - router bgp 65563 +# - no address-family ipv4 multicast +# - vrf site-1 +# - no address-family ipv6 multicast +# +# after: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only + +# Using deleted to remove all BGP AFs + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +- name: Delete all BGP AFs + cisco.nxos.nxos_bgp_address_family: + state: deleted + +# Task output +# ------------- +# before: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 +# +# commands: +# - router bgp 65563 +# - no address-family ipv4 multicast +# - vrf site-1 +# - no address-family ipv4 unicast +# - no address-family ipv6 multicast +# +# after: +# as_number: "65536" + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# Nexus9000v# + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_bgp_address_family: + config: + as_number: 65536 + address_family: + - afi: ipv4 + safi: multicast + networks: + - prefix: 192.0.2.32/27 + - prefix: 192.0.2.64/27 + route_map: rmap1 + nexthop: + route_map: rmap2 + trigger_delay: + critical_delay: 120 + non_critical_delay: 180 + - afi: ipv4 + safi: unicast + vrf: site-1 + default_information: + originate: True + aggregate_address: + - prefix: 203.0.113.0/24 + as_set: True + summary_only: True + - afi: ipv6 + safi: multicast + vrf: site-1 + redistribute: + - protocol: ospfv3 + id: 100 + route_map: rmap-ospf-1 + - protocol: eigrp + id: 101 + route_map: rmap-eigrp-1 + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - router bgp 65536 +# - address-family ipv4 multicast +# - nexthop route-map rmap2 +# - nexthop trigger-delay critical 120 non-critical 180 +# - network 192.0.2.32/27 +# - network 192.0.2.64/27 route-map rmap1 +# - vrf site-1 +# - address-family ipv4 unicast +# - default-information originate +# - aggregate-address 203.0.113.0/24 as-set summary-only +# - address-family ipv6 multicast +# - redistribute ospfv3 100 route-map rmap-ospf-1 +# - redistribute eigrp 101 route-map rmap-eigrp-1 + +# Using parsed + +# parsed.cfg +# ------------ +# router bgp 65536 +# address-family ipv4 multicast +# nexthop route-map rmap2 +# nexthop trigger-delay critical 120 non-critical 180 +# network 192.0.2.32/27 +# network 192.0.2.64/27 route-map rmap1 +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# aggregate-address 203.0.113.0/24 as-set summary-only +# address-family ipv6 multicast +# redistribute ospfv3 100 route-map rmap-ospf-1 +# redistribute eigrp 101 route-map rmap-eigrp-1 + +- name: Parse externally provided BGP AF config + cisco.nxos.nxos_bgp_address_family: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# as_number: "65536" +# address_family: +# - afi: ipv4 +# safi: multicast +# networks: +# - prefix: 192.0.2.32/27 +# - prefix: 192.0.2.64/27 +# route_map: rmap1 +# nexthop: +# route_map: rmap2 +# trigger_delay: +# critical_delay: 120 +# non_critical_delay: 180 +# - afi: ipv4 +# safi: unicast +# vrf: site-1 +# default_information: +# originate: True +# aggregate_address: +# - prefix: 203.0.113.0/24 +# as_set: True +# summary_only: True +# - afi: ipv6 +# safi: multicast +# vrf: site-1 +# redistribute: +# - id: "100" +# protocol: ospfv3 +# route_map: rmap-ospf-1 +# - id: "101" +# protocol: eigrp +# route_map: rmap-eigrp-1 +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.bgp_address_family.bgp_address_family import ( + Bgp_address_familyArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.bgp_address_family.bgp_address_family import ( + Bgp_address_family, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Bgp_address_familyArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Bgp_address_family(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_af.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_af.py new file mode 100644 index 00000000..290f241d --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_af.py @@ -0,0 +1,877 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bgp_af +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2023-02-24) Manages BGP Address-family configuration. +description: +- Manages BGP Address-family configurations on NX-OS switches. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +deprecated: + alternative: nxos_bgp_address_family + why: Updated module released with more functionality. + removed_at_date: '2023-02-24' +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) removes the whole BGP ASN configuration +- Default, where supported, restores params default value. +options: + asn: + description: + - BGP autonomous system number. Valid values are String, Integer in ASPLAIN or + ASDOT notation. + required: true + type: str + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing the global bgp. + default: 'default' + type: str + afi: + description: + - Address Family Identifier. + required: true + choices: + - ipv4 + - ipv6 + - vpnv4 + - vpnv6 + - l2vpn + type: str + safi: + description: + - Sub Address Family Identifier. + required: true + choices: + - unicast + - multicast + - evpn + type: str + additional_paths_install: + description: + - Install a backup path into the forwarding table and provide prefix independent + convergence (PIC) in case of a PE-CE link failure. + type: bool + additional_paths_receive: + description: + - Enables the receive capability of additional paths for all of the neighbors + under this address family for which the capability has not been disabled. + type: bool + additional_paths_selection: + description: + - Configures the capability of selecting additional paths for a prefix. Valid + values are a string defining the name of the route-map. + type: str + additional_paths_send: + description: + - Enables the send capability of additional paths for all of the neighbors under + this address family for which the capability has not been disabled. + type: bool + advertise_l2vpn_evpn: + description: + - Advertise evpn routes. + type: bool + client_to_client: + description: + - Configure client-to-client route reflection. + type: bool + dampen_igp_metric: + description: + - Specify dampen value for IGP metric-related changes, in seconds. Valid values + are integer and keyword 'default'. + type: str + dampening_state: + description: + - Enable/disable route-flap dampening. + type: bool + dampening_half_time: + description: + - Specify decay half-life in minutes for route-flap dampening. Valid values are + integer and keyword 'default'. + type: str + dampening_max_suppress_time: + description: + - Specify max suppress time for route-flap dampening stable route. Valid values + are integer and keyword 'default'. + type: str + dampening_reuse_time: + description: + - Specify route reuse time for route-flap dampening. Valid values are integer + and keyword 'default'. + type: str + dampening_routemap: + description: + - Specify route-map for route-flap dampening. Valid values are a string defining + the name of the route-map. + type: str + dampening_suppress_time: + description: + - Specify route suppress time for route-flap dampening. Valid values are integer + and keyword 'default'. + type: str + default_information_originate: + description: + - Default information originate. + type: bool + default_metric: + description: + - Sets default metrics for routes redistributed into BGP. Valid values are Integer + or keyword 'default' + type: str + distance_ebgp: + description: + - Sets the administrative distance for eBGP routes. Valid values are Integer or + keyword 'default'. + type: str + distance_ibgp: + description: + - Sets the administrative distance for iBGP routes. Valid values are Integer or + keyword 'default'. + type: str + distance_local: + description: + - Sets the administrative distance for local BGP routes. Valid values are Integer + or keyword 'default'. + type: str + inject_map: + description: + - An array of route-map names which will specify prefixes to inject. Each array + entry must first specify the inject-map name, secondly an exist-map name, and + optionally the copy-attributes keyword which indicates that attributes should + be copied from the aggregate. For example [['lax_inject_map', 'lax_exist_map'], + ['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'], ['fsd_inject_map', 'fsd_exist_map']]. + type: list + elements: list + maximum_paths: + description: + - Configures the maximum number of equal-cost paths for load sharing. Valid value + is an integer in the range 1-64. + type: str + maximum_paths_ibgp: + description: + - Configures the maximum number of ibgp equal-cost paths for load sharing. Valid + value is an integer in the range 1-64. + type: str + networks: + description: + - Networks to configure. Valid value is a list of network prefixes to advertise. + The list must be in the form of an array. Each entry in the array must include + a prefix address and an optional route-map. For example [['10.0.0.0/16', 'routemap_LA'], + ['192.168.1.1', 'Chicago'], ['192.168.2.0/24'], ['192.168.3.0/24', 'routemap_NYC']]. + type: list + elements: list + next_hop_route_map: + description: + - Configure a route-map for valid nexthops. Valid values are a string defining + the name of the route-map. + type: str + redistribute: + description: + - A list of redistribute directives. Multiple redistribute entries are allowed. + The list must be in the form of a nested array. the first entry of each array + defines the source-protocol to redistribute from; the second entry defines a + route-map name. A route-map is highly advised but may be optional on some platforms, + in which case it may be omitted from the array list. For example [['direct', + 'rm_direct'], ['lisp', 'rm_lisp']]. + type: list + elements: list + suppress_inactive: + description: + - Advertises only active routes to peers. + type: bool + table_map: + description: + - Apply table-map to filter routes downloaded into URIB. Valid values are a string. + type: str + table_map_filter: + description: + - Filters routes rejected by the route-map and does not download them to the RIB. + type: bool + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str + retain_route_target: + description: + - Retains all of the routes or the routes which are part of configured route-map. + Valid values are route-map names or keyword C(all) or keyword C(default). C(all) + retains all the routes regardless of Target-VPN community. C(default) will disable + the retain route target option. If you are using route-map name please ensure + that the name is not same as C(all) and C(default). + type: str + version_added: 1.1.0 +""" +EXAMPLES = """ +# configure a simple address-family +- cisco.nxos.nxos_bgp_af: + asn: 65535 + vrf: TESTING + afi: ipv4 + safi: unicast + advertise_l2vpn_evpn: true + state: present + retain_route_target: all +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf TESTING", + "address-family ipv4 unicast", "advertise l2vpn evpn", + "retain route-target all"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +BOOL_PARAMS = [ + "additional_paths_install", + "additional_paths_receive", + "additional_paths_send", + "advertise_l2vpn_evpn", + "dampening_state", + "default_information_originate", + "suppress_inactive", +] +PARAM_TO_DEFAULT_KEYMAP = { + "maximum_paths": "1", + "maximum_paths_ibgp": "1", + "client_to_client": True, + "distance_ebgp": "20", + "distance_ibgp": "200", + "distance_local": "220", + "dampen_igp_metric": "600", +} +PARAM_TO_COMMAND_KEYMAP = { + "asn": "router bgp", + "afi": "address-family", + "safi": "address-family", + "additional_paths_install": "additional-paths install backup", + "additional_paths_receive": "additional-paths receive", + "additional_paths_selection": "additional-paths selection route-map", + "additional_paths_send": "additional-paths send", + "advertise_l2vpn_evpn": "advertise l2vpn evpn", + "client_to_client": "client-to-client reflection", + "dampen_igp_metric": "dampen-igp-metric", + "dampening_state": "dampening", + "dampening_half_time": "dampening", + "dampening_max_suppress_time": "dampening", + "dampening_reuse_time": "dampening", + "dampening_routemap": "dampening route-map", + "dampening_suppress_time": "dampening", + "default_information_originate": "default-information originate", + "default_metric": "default-metric", + "distance_ebgp": "distance", + "distance_ibgp": "distance", + "distance_local": "distance", + "inject_map": "inject-map", + "maximum_paths": "maximum-paths", + "maximum_paths_ibgp": "maximum-paths ibgp", + "networks": "network", + "redistribute": "redistribute", + "next_hop_route_map": "nexthop route-map", + "suppress_inactive": "suppress-inactive", + "table_map": "table-map", + "table_map_filter": "table-map-filter", + "vrf": "vrf", + "retain_route_target": "retain route-target", +} +DAMPENING_PARAMS = [ + "dampening_half_time", + "dampening_suppress_time", + "dampening_reuse_time", + "dampening_max_suppress_time", +] + + +def get_value(arg, config, module): + command = PARAM_TO_COMMAND_KEYMAP[arg] + command_val_re = re.compile(r"(?:{0}\s)(?P<value>.*)$".format(command), re.M) + has_command_val = command_val_re.search(config) + + if arg in ["networks", "redistribute", "inject_map"]: + value = [] + for ele in command_val_re.findall(config): + tl = ele.split() + if "exist-map" in tl: + tl.remove("exist-map") + elif "route-map" in tl: + tl.remove("route-map") + value.append(tl) + + elif command == "distance": + distance_re = r".*distance\s(?P<d_ebgp>\w+)\s(?P<d_ibgp>\w+)\s(?P<d_local>\w+)" + match_distance = re.match(distance_re, config, re.DOTALL) + + value = "" + if match_distance: + distance_group = match_distance.groupdict() + + if arg == "distance_ebgp": + value = distance_group["d_ebgp"] + elif arg == "distance_ibgp": + value = distance_group["d_ibgp"] + elif arg == "distance_local": + value = distance_group["d_local"] + + elif command.split()[0] == "dampening": + value = "" + if arg == "dampen_igp_metric" or arg == "dampening_routemap": + if command in config: + value = has_command_val.group("value") + else: + dampening_re = r".*dampening\s(?P<half>\w+)\s(?P<reuse>\w+)\s(?P<suppress>\w+)\s(?P<max_suppress>\w+)" + match_dampening = re.match(dampening_re, config, re.DOTALL) + if match_dampening: + dampening_group = match_dampening.groupdict() + + if arg == "dampening_half_time": + value = dampening_group["half"] + elif arg == "dampening_reuse_time": + value = dampening_group["reuse"] + elif arg == "dampening_suppress_time": + value = dampening_group["suppress"] + elif arg == "dampening_max_suppress_time": + value = dampening_group["max_suppress"] + else: + if arg == "dampening_state": + value = True if "dampening" in config else False + elif arg == "table_map_filter": + tmf_regex = re.compile(r"\s+table-map.*filter$", re.M) + value = False + if tmf_regex.search(config): + value = True + + elif arg == "table_map": + tm_regex = re.compile(r"(?:table-map\s)(?P<value>\S+)(\sfilter)?$", re.M) + has_tablemap = tm_regex.search(config) + value = "" + if has_tablemap: + value = has_tablemap.group("value") + + elif arg == "client_to_client": + no_command_re = re.compile(r"^\s+no\s{0}\s*$".format(command), re.M) + value = True + + if no_command_re.search(config): + value = False + + elif arg == "retain_route_target": + value = "" + if command in config: + route_target = has_command_val.group("value") + if route_target: + if route_target == "all": + value = "all" + elif "route-map" in route_target: + value = route_target.replace("route-map ", "") + + elif arg in BOOL_PARAMS: + command_re = re.compile(r"^\s+{0}\s*$".format(command), re.M) + value = False + + if command_re.search(config): + value = True + + else: + value = "" + + if has_command_val: + value = has_command_val.group("value") + + return value + + +def get_existing(module, args, warnings): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + + asn_regex = re.compile(r".*router\sbgp\s(?P<existing_asn>\d+(\.\d+)?).*", re.DOTALL) + match_asn = asn_regex.match(str(netcfg)) + + if match_asn: + existing_asn = match_asn.group("existing_asn") + parents = ["router bgp {0}".format(existing_asn)] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("address-family {0} {1}".format(module.params["afi"], module.params["safi"])) + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg not in ["asn", "afi", "safi", "vrf"]: + gv = get_value(arg, config, module) + if gv: + existing[arg] = gv + else: + if arg != "client_to_client" and arg in PARAM_TO_DEFAULT_KEYMAP.keys(): + existing[arg] = PARAM_TO_DEFAULT_KEYMAP.get(arg) + else: + existing[arg] = gv + + existing["asn"] = existing_asn + existing["afi"] = module.params["afi"] + existing["safi"] = module.params["safi"] + existing["vrf"] = module.params["vrf"] + else: + warnings.append( + "The BGP process {0} didn't exist but the task just created it.".format( + module.params["asn"], + ), + ) + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = value + + return new_dict + + +def fix_proposed(module, proposed, existing): + commands = list() + command = "" + fixed_proposed = {} + for key, value in proposed.items(): + if key in DAMPENING_PARAMS: + if value != "default": + command = "dampening {0} {1} {2} {3}".format( + proposed.get("dampening_half_time"), + proposed.get("dampening_reuse_time"), + proposed.get("dampening_suppress_time"), + proposed.get("dampening_max_suppress_time"), + ) + else: + if existing.get(key): + command = "no dampening {0} {1} {2} {3}".format( + existing["dampening_half_time"], + existing["dampening_reuse_time"], + existing["dampening_suppress_time"], + existing["dampening_max_suppress_time"], + ) + if "default" in command: + command = "" + elif key.startswith("distance"): + command = "distance {0} {1} {2}".format( + proposed.get("distance_ebgp"), + proposed.get("distance_ibgp"), + proposed.get("distance_local"), + ) + else: + fixed_proposed[key] = value + + if command: + if command not in commands: + commands.append(command) + + return fixed_proposed, commands + + +def default_existing(existing_value, key, value): + commands = [] + if key == "network": + for network in existing_value: + if len(network) == 2: + commands.append("no network {0} route-map {1}".format(network[0], network[1])) + elif len(network) == 1: + commands.append("no network {0}".format(network[0])) + + elif key == "inject-map": + for maps in existing_value: + if len(maps) == 2: + commands.append("no inject-map {0} exist-map {1}".format(maps[0], maps[1])) + elif len(maps) == 3: + commands.append( + "no inject-map {0} exist-map {1} " "copy-attributes".format(maps[0], maps[1]), + ) + + elif key == "redistribute": + for maps in existing_value: + commands.append("no redistribute {0} route-map {1}".format(maps[0], maps[1])) + + elif key == "retain route-target": + if existing_value == "all": + commands.append("no {0} {1}".format(key, existing_value)) + elif existing_value != "default": + commands.append("no {0} route-map {1}".format(key, existing_value)) + + else: + commands.append("no {0} {1}".format(key, existing_value)) + return commands + + +def get_network_command(existing, key, value): + commands = [] + existing_networks = existing.get("networks", []) + for inet in value: + if not isinstance(inet, list): + inet = [inet] + if inet not in existing_networks: + if len(inet) == 1: + command = "{0} {1}".format(key, inet[0]) + elif len(inet) == 2: + command = "{0} {1} route-map {2}".format(key, inet[0], inet[1]) + if command: + commands.append(command) + for enet in existing_networks: + if enet not in value: + if len(enet) == 1: + command = "no {0} {1}".format(key, enet[0]) + elif len(enet) == 2: + command = "no {0} {1} route-map {2}".format(key, enet[0], enet[1]) + if command: + commands.append(command) + return commands + + +def get_inject_map_command(existing, key, value): + commands = [] + existing_maps = existing.get("inject_map", []) + for maps in value: + if not isinstance(maps, list): + maps = [maps] + if maps not in existing_maps: + if len(maps) == 2: + command = "inject-map {0} exist-map {1}".format(maps[0], maps[1]) + elif len(maps) == 3: + command = "inject-map {0} exist-map {1} " "copy-attributes".format(maps[0], maps[1]) + if command: + commands.append(command) + for emaps in existing_maps: + if emaps not in value: + if len(emaps) == 2: + command = "no inject-map {0} exist-map {1}".format(emaps[0], emaps[1]) + elif len(emaps) == 3: + command = "no inject-map {0} exist-map {1} " "copy-attributes".format( + emaps[0], + emaps[1], + ) + if command: + commands.append(command) + return commands + + +def get_redistribute_command(existing, key, value): + commands = [] + existing_rules = existing.get("redistribute", []) + for rule in value: + if not isinstance(rule, list): + rule = [rule] + if rule not in existing_rules: + command = "redistribute {0} route-map {1}".format(rule[0], rule[1]) + commands.append(command) + for erule in existing_rules: + if erule not in value: + command = "no redistribute {0} route-map {1}".format(erule[0], erule[1]) + commands.append(command) + return commands + + +def get_table_map_command(module, existing, key, value): + commands = [] + if key == "table-map": + if value != "default": + command = "{0} {1}".format(key, module.params["table_map"]) + if ( + module.params["table_map_filter"] is not None + and module.params["table_map_filter"] != "default" + ): + command += " filter" + commands.append(command) + else: + if existing.get("table_map"): + command = "no {0} {1}".format(key, existing.get("table_map")) + commands.append(command) + return commands + + +def get_retain_route_target_command(existing, key, value): + commands = [] + if key == "retain route-target": + if value != "default": + if value == "all": + command = "{0} {1}".format(key, value) + else: + command = "{0} route-map {1}".format(key, value) + else: + existing_value = existing.get("retain_route_target") + if existing_value == "all": + command = "no {0} {1}".format(key, existing_value) + else: + command = "no {0} route-map {1}".format(key, existing_value) + commands.append(command) + return commands + + +def get_default_table_map_filter(existing): + commands = [] + existing_table_map_filter = existing.get("table_map_filter") + if existing_table_map_filter: + existing_table_map = existing.get("table_map") + if existing_table_map: + command = "table-map {0}".format(existing_table_map) + commands.append(command) + return commands + + +def state_present(module, existing, proposed, candidate): + fixed_proposed, commands = fix_proposed(module, proposed, existing) + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, fixed_proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.items(): + if key == "address-family": + addr_family_command = "address-family {0} {1}".format( + module.params["afi"], + module.params["safi"], + ) + if addr_family_command not in commands: + commands.append(addr_family_command) + + elif key.startswith("table-map"): + table_map_commands = get_table_map_command(module, existing, key, value) + if table_map_commands: + commands.extend(table_map_commands) + + elif value is True: + commands.append(key) + + elif value is False: + commands.append("no {0}".format(key)) + + elif value == "default": + if key in PARAM_TO_DEFAULT_KEYMAP: + commands.append("{0} {1}".format(key, PARAM_TO_DEFAULT_KEYMAP[key])) + + elif existing_commands.get(key): + if key == "table-map-filter": + default_tmf_command = get_default_table_map_filter(existing) + + if default_tmf_command: + commands.extend(default_tmf_command) + else: + existing_value = existing_commands.get(key) + default_command = default_existing(existing_value, key, value) + if default_command: + commands.extend(default_command) + else: + if key == "network": + network_commands = get_network_command(existing, key, value) + if network_commands: + commands.extend(network_commands) + + elif key == "inject-map": + inject_map_commands = get_inject_map_command(existing, key, value) + if inject_map_commands: + commands.extend(inject_map_commands) + + elif key == "redistribute": + redistribute_commands = get_redistribute_command(existing, key, value) + if redistribute_commands: + commands.extend(redistribute_commands) + + elif key == "retain route-target": + retain_route_target_commands = get_retain_route_target_command(existing, key, value) + if retain_route_target_commands: + commands.extend(retain_route_target_commands) + + else: + command = "{0} {1}".format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + addr_family_command = "address-family {0} {1}".format( + module.params["afi"], + module.params["safi"], + ) + parents.append(addr_family_command) + if addr_family_command in commands: + commands.remove(addr_family_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + commands.append("no address-family {0} {1}".format(module.params["afi"], module.params["safi"])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type="str"), + vrf=dict(required=False, type="str", default="default"), + safi=dict(required=True, type="str", choices=["unicast", "multicast", "evpn"]), + afi=dict( + required=True, + type="str", + choices=["ipv4", "ipv6", "vpnv4", "vpnv6", "l2vpn"], + ), + additional_paths_install=dict(required=False, type="bool"), + additional_paths_receive=dict(required=False, type="bool"), + additional_paths_selection=dict(required=False, type="str"), + additional_paths_send=dict(required=False, type="bool"), + advertise_l2vpn_evpn=dict(required=False, type="bool"), + client_to_client=dict(required=False, type="bool"), + dampen_igp_metric=dict(required=False, type="str"), + dampening_state=dict(required=False, type="bool"), + dampening_half_time=dict(required=False, type="str"), + dampening_max_suppress_time=dict(required=False, type="str"), + dampening_reuse_time=dict(required=False, type="str"), + dampening_routemap=dict(required=False, type="str"), + dampening_suppress_time=dict(required=False, type="str"), + default_information_originate=dict(required=False, type="bool"), + default_metric=dict(required=False, type="str"), + distance_ebgp=dict(required=False, type="str"), + distance_ibgp=dict(required=False, type="str"), + distance_local=dict(required=False, type="str"), + inject_map=dict(required=False, type="list", elements="list"), + maximum_paths=dict(required=False, type="str"), + maximum_paths_ibgp=dict(required=False, type="str"), + networks=dict(required=False, type="list", elements="list"), + next_hop_route_map=dict(required=False, type="str"), + redistribute=dict(required=False, type="list", elements="list"), + suppress_inactive=dict(required=False, type="bool"), + table_map=dict(required=False, type="str"), + table_map_filter=dict(required=False, type="bool"), + state=dict(choices=["present", "absent"], default="present", required=False), + retain_route_target=dict(required=False, type="str"), + ) + + mutually_exclusive = [ + ("dampening_state", "dampening_routemap"), + ("dampening_state", "dampening_half_time"), + ("dampening_state", "dampening_suppress_time"), + ("dampening_state", "dampening_reuse_time"), + ("dampening_state", "dampening_max_suppress_time"), + ("dampening_routemap", "dampening_half_time"), + ("dampening_routemap", "dampening_suppress_time"), + ("dampening_routemap", "dampening_reuse_time"), + ("dampening_routemap", "dampening_max_suppress_time"), + ] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_together=[ + DAMPENING_PARAMS, + ["distance_ibgp", "distance_ebgp", "distance_local"], + ], + supports_check_mode=True, + ) + + warnings = list() + result = dict(changed=False, warnings=warnings) + + state = module.params["state"] + + if module.params["advertise_l2vpn_evpn"]: + if module.params["vrf"] == "default": + module.fail_json( + msg="It is not possible to advertise L2VPN " + "EVPN in the default VRF. Please specify " + "another one.", + vrf=module.params["vrf"], + ) + + if module.params["table_map_filter"] and not module.params["table_map"]: + module.fail_json(msg="table_map param is needed when using" " table_map_filter filter.") + + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args, warnings) + + if existing.get("asn") and state == "present": + if existing.get("asn") != module.params["asn"]: + module.fail_json( + msg="Another BGP ASN already exists.", + proposed_asn=module.params["asn"], + existing_asn=existing.get("asn"), + ) + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + for arg in ["networks", "inject_map", "redistribute"]: + if proposed_args.get(arg): + if proposed_args[arg][0] == "default": + proposed_args[arg] = "default" + + proposed = {} + for key, value in proposed_args.items(): + if key not in ["asn", "vrf"]: + if str(value).lower() == "default": + value = PARAM_TO_DEFAULT_KEYMAP.get(key, "default") + if existing.get(key) != value: + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + if state == "present": + state_present(module, existing, proposed, candidate) + elif state == "absent" and existing: + state_absent(module, candidate) + + if candidate: + candidate = candidate.items_text() + if not module.check_mode: + load_config(module, candidate) + result["changed"] = True + result["commands"] = candidate + else: + result["commands"] = [] + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_global.py new file mode 100644 index 00000000..b3072d66 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_global.py @@ -0,0 +1,1694 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_bgp_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_bgp_global +short_description: BGP Global resource module. +description: +- This module manages global BGP configuration on devices running Cisco NX-OS. +version_added: 1.4.0 +notes: +- Tested against NX-OS 9.3.6. +- Unsupported for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^router bgp'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of BGP process configuration. + type: dict + suboptions: + as_number: + description: Autonomous System Number of the router. + type: str + affinity_group: + description: Configure an affinity group. + type: dict + suboptions: + group_id: + description: Affinity Group ID. + type: int + bestpath: &bestpath + description: Define the default bestpath selection algorithm. + type: dict + suboptions: + always_compare_med: + description: Compare MED on paths from different AS. + type: bool + as_path: + description: AS-Path. + type: dict + suboptions: + ignore: + description: Ignore AS-Path during bestpath selection. + type: bool + multipath_relax: + description: Relax AS-Path restriction when choosing multipaths. + type: bool + compare_neighborid: + description: When more paths are available than max path config, use neighborid as tie-breaker. + type: bool + compare_routerid: + description: Compare router-id for identical EBGP paths. + type: bool + cost_community_ignore: + description: Ignore cost communities in bestpath selection. + type: bool + igp_metric_ignore: + description: Ignore IGP metric for next-hop during bestpath selection. + type: bool + med: + description: MED + type: dict + suboptions: + confed: + description: Compare MED only from paths originated from within a confederation. + type: bool + missing_as_worst: + description: Treat missing MED as highest MED. + type: bool + non_deterministic: + description: Not always pick the best-MED path among paths from same AS. + type: bool + cluster_id: &cluster_id + description: Configure Route Reflector Cluster-ID. + type: str + confederation: &confederation + description: AS confederation parameters. + type: dict + suboptions: + identifier: + description: Set routing domain confederation AS. + type: str + peers: + description: Peer ASs in BGP confederation. + type: list + elements: str + disable_policy_batching: + description: Disable batching evaluation of outbound policy for a peer. + type: dict + suboptions: + set: + description: Set policy batching. + type: bool + ipv4: + description: IPv4 address-family settings. + type: dict + suboptions: + prefix_list: + description: Name of prefix-list to apply. + type: str + ipv6: + description: IPv6 address-family settings. + type: dict + suboptions: + prefix_list: + description: Name of prefix-list to apply. + type: str + nexthop: + description: Batching based on nexthop. + type: bool + dynamic_med_interval: + description: Sets the interval for dampening of med changes. + type: int + enforce_first_as: + description: Enforce neighbor AS is the first AS in AS-PATH attribute (EBGP). + type: bool + enhanced_error: + description: Enable BGP Enhanced error handling. + type: bool + fabric_soo: + description: Fabric site of origin. + type: str + fast_external_fallover: + description: Immediately reset the session if the link to a directly connected BGP peer goes down. + type: bool + flush_routes: + description: Flush routes in RIB upon controlled restart. + type: bool + graceful_restart: &graceful_restart + description: Configure Graceful Restart functionality. + type: dict + suboptions: + set: + description: Enable graceful-restart. + type: bool + restart_time: + description: Maximum time for restart advertised to peers. + type: int + stalepath_time: + description: Maximum time to keep a restarting peer's stale routes. + type: int + helper: + description: Configure Graceful Restart Helper mode functionality. + type: bool + graceful_shutdown: + description: Graceful-shutdown for BGP protocol. + type: dict + suboptions: + activate: + description: Send graceful-shutdown community on all routes. + type: dict + suboptions: + set: + description: Activiate graceful-shutdown. + type: bool + route_map: + description: Apply route-map to modify attributes for outbound. + type: str + aware: + description: Lower preference of routes carrying graceful-shutdown community. + type: bool + isolate: + description: Isolate this router from BGP perspective. + type: dict + suboptions: + set: + description: Withdraw remote BGP routes to isolate this router. + type: bool + include_local: + description: Withdraw both local and remote BGP routes. + type: bool + log_neighbor_changes: &log_nbr + description: Log a message for neighbor up/down event. + type: bool + maxas_limit: &maxas_limit + description: Allow AS-PATH attribute from EBGP neighbor imposing a limit on number of ASes. + type: int + neighbors: &nbr + description: Configure BGP neighbors. + type: list + elements: dict + suboptions: + neighbor_address: + description: IP address/Prefix of the neighbor or interface. + type: str + required: True + bfd: + description: Bidirectional Fast Detection for the neighbor. + type: dict + suboptions: + set: + description: Set BFD for this neighbor. + type: bool + singlehop: + description: Single-hop session. + type: bool + multihop: + description: Multihop session. + type: dict + suboptions: + set: + description: Set BFD multihop. + type: bool + interval: + description: Configure BFD session interval parameters. + type: dict + suboptions: + tx_interval: + description: TX interval in milliseconds. + type: int + min_rx_interval: + description: Minimum RX interval. + type: int + multiplier: + description: Detect Multiplier. + type: int + neighbor_affinity_group: + description: Configure an affinity group. + type: dict + suboptions: + group_id: + description: Affinity Group ID. + type: int + bmp_activate_server: + description: Specify server ID for activating BMP monitoring for the peer. + type: int + capability: + description: Capability. + type: dict + suboptions: + suppress_4_byte_as: + description: Suppress 4-byte AS Capability. + type: bool + description: + description: Neighbor specific descripion. + type: str + disable_connected_check: + description: Disable check for directly connected peer. + type: bool + dont_capability_negotiate: + description: Don't negotiate capability with this neighbor. + type: bool + dscp: + description: Set dscp value for tcp transport. + type: str + dynamic_capability: + description: Dynamic Capability + type: bool + ebgp_multihop: + description: Specify multihop TTL for remote peer. + type: int + graceful_shutdown: + description: Graceful-shutdown for this neighbor. + type: dict + suboptions: + activate: + description: Send graceful-shutdown community. + type: dict + suboptions: + set: + description: Set activate. + type: bool + route_map: + description: Apply route-map to modify attributes for outbound. + type: str + inherit: + description: Inherit a template. + type: dict + suboptions: + peer: + description: Peer template to inherit. + type: str + peer_session: + description: Peer-session template to inherit. + type: str + local_as: + description: Specify the local-as number for the eBGP neighbor. + type: str + log_neighbor_changes: + description: Log message for neighbor up/down event. + type: dict + suboptions: + set: + description: + - Set log-neighbor-changes. + type: bool + disable: + description: + - Disable logging of neighbor up/down event. + type: bool + low_memory: + description: Behaviour in low memory situations. + type: dict + suboptions: + exempt: + description: Do not shutdown this peer when under memory pressure. + type: bool + password: + description: Configure a password for neighbor. + type: dict + suboptions: + encryption: + description: + - 0 specifies an UNENCRYPTED neighbor password. + - 3 specifies an 3DES ENCRYPTED neighbor password will follow. + - 7 specifies a Cisco type 7 ENCRYPTED neighbor password will follow. + type: int + key: + description: Authentication password. + type: str + path_attribute: + description: BGP path attribute optional filtering. + type: list + elements: dict + suboptions: + action: + description: Action. + type: str + choices: ["discard", "treat-as-withdraw"] + type: + description: Path attribute type + type: int + range: + description: Path attribute range. + type: dict + suboptions: + start: + description: Path attribute range start value. + type: int + end: + description: Path attribute range end value. + type: int + peer_type: + description: Neighbor facing + type: str + choices: ["fabric-border-leaf", "fabric-external"] + remote_as: + description: Specify Autonomous System Number of the neighbor. + type: str + remove_private_as: + description: Remove private AS number from outbound updates. + type: dict + suboptions: + set: + description: Remove private AS. + type: bool + replace_as: + description: Replace. + type: bool + all: + description: All. + type: bool + shutdown: + description: Administratively shutdown this neighbor. + type: bool + timers: + description: Configure keepalive and hold timers. + type: dict + suboptions: + keepalive: + description: Keepalive interval (seconds). + type: int + holdtime: + description: Holdtime (seconds). + type: int + transport: + description: BGP transport connection. + type: dict + suboptions: + connection_mode: + description: Specify type of connection. + type: dict + suboptions: + passive: + description: Allow passive connection setup only. + type: bool + ttl_security: + description: Enable TTL Security Mechanism. + type: dict + suboptions: + hops: + description: Specify hop count for remote peer. + type: int + update_source: + description: Specify source of BGP session and updates. + type: str + neighbor_down: &nbr_down + description: Handle BGP neighbor down event, due to various reasons. + type: dict + suboptions: + fib_accelerate: + description: Accelerate the hardware updates for IP/IPv6 adjacencies for neighbor. + type: bool + nexthop: + description: Nexthop resolution options. + type: dict + suboptions: + suppress_default_resolution: + description: Prohibit use of default route for nexthop address resolution. + type: bool + rd: + description: Secondary Route Distinguisher for vxlan multisite border gateway. + type: dict + suboptions: + dual: + description: Generate Secondary RD for all VRFs and L2VNIs. + type: bool + id: + description: Specify 2 byte value for ID. + type: int + reconnect_interval: &reconn_intv + description: Configure connection reconnect interval. + type: int + router_id: &rtr_id + description: Specify the IP address to use as router-id. + type: str + shutdown: &shtdwn + description: Administratively shutdown BGP protocol. + type: bool + suppress_fib_pending: &suppr + description: Advertise only routes that are programmed in hardware to peers. + type: bool + timers: &timers + description: Configure bgp related timers. + type: dict + suboptions: + bestpath_limit: + description: Configure timeout for first bestpath after restart. + type: dict + suboptions: + timeout: + description: Bestpath timeout (seconds). + type: int + always: + description: Configure update-delay-always option. + type: bool + bgp: + description: Configure different bgp keepalive and holdtimes. + type: dict + suboptions: + keepalive: + description: Keepalive interval (seconds). + type: int + holdtime: + description: Holdtime (seconds). + type: int + prefix_peer_timeout: + description: Prefix Peer timeout (seconds). + type: int + prefix_peer_wait: + description: Configure wait timer for a prefix peer. + type: int + vrfs: + description: Virtual Router Context configurations. + type: list + elements: dict + suboptions: + vrf: + description: VRF name. + type: str + allocate_index: + description: Configure allocate-index. + type: int + bestpath: *bestpath + cluster_id: *cluster_id + confederation: *confederation + graceful_restart: *graceful_restart + local_as: + description: Specify the local-as for this vrf. + type: str + log_neighbor_changes: *log_nbr + maxas_limit: *maxas_limit + neighbors: *nbr + neighbor_down: *nbr_down + reconnect_interval: *reconn_intv + router_id: *rtr_id + timers: *timers + state: + description: + - The state the configuration should be left in. + - State I(purged) removes all the BGP configurations from the + target device. Use caution with this state. + - State I(deleted) only removes BGP attributes that this modules + manages and does not negate the BGP process completely. Thereby, preserving + address-family related configurations under BGP context. + - Running states I(deleted) and I(replaced) will result in an error if there + are address-family configuration lines present under a neighbor, + or a vrf context that is to be removed. Please use the + M(cisco.nxos.nxos_bgp_af) or M(cisco.nxos.nxos_bgp_neighbor_af) + modules for prior cleanup. + - States I(merged) and I(replaced) will result in a failure if BGP is already configured + with a different ASN than what is provided in the task. In such cases, please use + state I(purged) to remove the existing BGP process and proceed further. + - Refer to examples for more details. + type: str + choices: + - merged + - replaced + - deleted + - purged + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# Nexus9000v# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_bgp_global: + config: + as_number: 65563 + router_id: 192.168.1.1 + bestpath: + as_path: + multipath_relax: True + compare_neighborid: True + cost_community_ignore: True + confederation: + identifier: 42 + peers: + - 65020 + - 65030 + - 65040 + log_neighbor_changes: True + maxas_limit: 20 + neighbors: + - neighbor_address: 192.168.1.100 + neighbor_affinity_group: + group_id: 160 + bmp_activate_server: 1 + remote_as: 65563 + description: NBR-1 + low_memory: + exempt: True + - neighbor_address: 192.168.1.101 + remote_as: 65563 + password: + encryption: 7 + key: 12090404011C03162E + neighbor_down: + fib_accelerate: True + vrfs: + - vrf: site-1 + allocate_index: 5000 + local_as: 200 + log_neighbor_changes: True + neighbors: + - neighbor_address: 198.51.100.1 + description: site-1-nbr-1 + password: + encryption: 3 + key: 13D4D3549493D2877B1DC116EE27A6BE + remote_as: 65562 + - neighbor_address: 198.51.100.2 + remote_as: 65562 + description: site-1-nbr-2 + - vrf: site-2 + local_as: 300 + log_neighbor_changes: True + neighbors: + - neighbor_address: 203.0.113.2 + description: site-2-nbr-1 + password: + encryption: 3 + key: AF92F4C16A0A0EC5BDF56CF58BC030F6 + remote_as: 65568 + neighbor_down: + fib_accelerate: True + +# Task output +# ------------- +# before: {} +# +# commands: +# - router bgp 65563 +# - bestpath as-path multipath-relax +# - bestpath compare-neighborid +# - bestpath cost-community ignore +# - confederation identifier 42 +# - log-neighbor-changes +# - maxas-limit 20 +# - neighbor-down fib-accelerate +# - router-id 192.168.1.1 +# - confederation peers 65020 65030 65040 +# - neighbor 192.168.1.100 +# - remote-as 65563 +# - affinity-group 160 +# - bmp-activate-server 1 +# - description NBR-1 +# - low-memory exempt +# - neighbor 192.168.1.101 +# - remote-as 65563 +# - password 7 12090404011C03162E +# - vrf site-1 +# - allocate-index 5000 +# - local-as 200 +# - log-neighbor-changes +# - neighbor 198.51.100.1 +# - remote-as 65562 +# - description site-1-nbr-1 +# - password 3 13D4D3549493D2877B1DC116EE27A6BE +# - neighbor 198.51.100.2 +# - remote-as 65562 +# - description site-1-nbr-2 +# - vrf site-2 +# - local-as 300 +# - log-neighbor-changes +# - neighbor-down fib-accelerate +# - neighbor 203.0.113.2 +# - remote-as 65568 +# - description site-2-nbr-1 +# - password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 +# +# after: +# as_number: '65563' +# bestpath: +# as_path: +# multipath_relax: true +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65040' +# log_neighbor_changes: true +# maxas_limit: 20 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# - neighbor_address: 192.168.1.101 +# password: +# encryption: 7 +# key: 12090404011C03162E +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - allocate_index: 5000 +# local_as: '200' +# log_neighbor_changes: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 198.51.100.1 +# password: +# encryption: 3 +# key: 13D4D3549493D2877B1DC116EE27A6BE +# remote_as: '65562' +# - description: site-1-nbr-2 +# neighbor_address: 198.51.100.2 +# remote_as: '65562' +# vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - description: site-2-nbr-1 +# neighbor_address: 203.0.113.2 +# password: +# encryption: 3 +# key: AF92F4C16A0A0EC5BDF56CF58BC030F6 +# remote_as: '65568' +# vrf: site-2 + + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65040 +# bestpath as-path multipath-relax +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 20 +# log-neighbor-changes +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# neighbor 192.168.1.101 +# remote-as 65563 +# password 7 12090404011C03162E +# vrf site-1 +# local-as 200 +# log-neighbor-changes +# allocate-index 5000 +# neighbor 198.51.100.1 +# remote-as 65562 +# description site-1-nbr-1 +# password 3 13D4D3549493D2877B1DC116EE27A6BE +# neighbor 198.51.100.2 +# remote-as 65562 +# description site-1-nbr-2 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# remote-as 65568 +# description site-2-nbr-1 +# password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +# Using replaced + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65040 +# bestpath as-path multipath-relax +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 20 +# log-neighbor-changes +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# neighbor 192.168.1.101 +# remote-as 65563 +# password 7 12090404011C03162E +# vrf site-1 +# local-as 200 +# log-neighbor-changes +# allocate-index 5000 +# neighbor 198.51.100.1 +# remote-as 65562 +# description site-1-nbr-1 +# password 3 13D4D3549493D2877B1DC116EE27A6BE +# neighbor 198.51.100.2 +# remote-as 65562 +# description site-1-nbr-2 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# remote-as 65568 +# description site-2-nbr-1 +# password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +- name: Replace BGP configuration with provided configuration + cisco.nxos.nxos_bgp_global: + config: + as_number: 65563 + router_id: 192.168.1.1 + bestpath: + compare_neighborid: True + cost_community_ignore: True + confederation: + identifier: 42 + peers: + - 65020 + - 65030 + - 65050 + maxas_limit: 40 + neighbors: + - neighbor_address: 192.168.1.100 + neighbor_affinity_group: + group_id: 160 + bmp_activate_server: 1 + remote_as: 65563 + description: NBR-1 + low_memory: + exempt: True + neighbor_down: + fib_accelerate: True + vrfs: + - vrf: site-2 + local_as: 300 + log_neighbor_changes: True + neighbors: + - neighbor_address: 203.0.113.2 + password: + encryption: 7 + key: 12090404011C03162E + neighbor_down: + fib_accelerate: True + state: replaced + +# Task output +# ------------- +# before: +# as_number: '65563' +# bestpath: +# as_path: +# multipath_relax: true +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65040' +# log_neighbor_changes: true +# maxas_limit: 20 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# - neighbor_address: 192.168.1.101 +# password: +# encryption: 7 +# key: 12090404011C03162E +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - allocate_index: 5000 +# local_as: '200' +# log_neighbor_changes: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 198.51.100.1 +# password: +# encryption: 3 +# key: 13D4D3549493D2877B1DC116EE27A6BE +# remote_as: '65562' +# - description: site-1-nbr-2 +# neighbor_address: 198.51.100.2 +# remote_as: '65562' +# vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - description: site-2-nbr-1 +# neighbor_address: 203.0.113.2 +# password: +# encryption: 3 +# key: AF92F4C16A0A0EC5BDF56CF58BC030F6 +# remote_as: '65568' +# vrf: site-2 +# +# commands: +# - router bgp 65563 +# - no bestpath as-path multipath-relax +# - no log-neighbor-changes +# - maxas-limit 40 +# - no confederation peers 65020 65030 65040 +# - confederation peers 65020 65030 65050 +# - no neighbor 192.168.1.101 +# - vrf site-2 +# - neighbor 203.0.113.2 +# - no remote-as 65568 +# - no description site-2-nbr-1 +# - password 7 12090404011C03162E +# - no vrf site-1 + +# after: +# as_number: '65563' +# bestpath: +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65050' +# maxas_limit: 40 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - neighbor_address: 203.0.113.2 +# password: +# encryption: 7 +# key: 12090404011C03162E +# vrf: site-2 +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65050 +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 40 +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# password 7 12090404011C03162E + +# Using deleted + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65040 +# bestpath as-path multipath-relax +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 20 +# log-neighbor-changes +# address-family ipv4 unicast +# default-metric 400 +# suppress-inactive +# default-information originate +# address-family ipv6 multicast +# wait-igp-convergence +# redistribute eigrp eigrp-1 route-map site-1-rmap +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# neighbor 192.168.1.101 +# remote-as 65563 +# password 7 12090404011C03162E +# vrf site-1 +# local-as 200 +# log-neighbor-changes +# allocate-index 5000 +# address-family ipv4 multicast +# maximum-paths 40 +# dampen-igp-metric 1200 +# neighbor 198.51.100.1 +# remote-as 65562 +# description site-1-nbr-1 +# password 3 13D4D3549493D2877B1DC116EE27A6BE +# neighbor 198.51.100.2 +# remote-as 65562 +# description site-1-nbr-2 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# remote-as 65568 +# description site-1-nbr-1 +# password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +- name: Delete BGP configurations handled by this module + cisco.nxos.nxos_bgp_global: + state: deleted + +# Task output +# ------------- + +# before: +# as_number: '65563' +# bestpath: +# as_path: +# multipath_relax: true +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65040' +# log_neighbor_changes: true +# maxas_limit: 20 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# - neighbor_address: 192.168.1.101 +# password: +# encryption: 7 +# key: 12090404011C03162E +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - allocate_index: 5000 +# local_as: '200' +# log_neighbor_changes: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 198.51.100.1 +# password: +# encryption: 3 +# key: 13D4D3549493D2877B1DC116EE27A6BE +# remote_as: '65562' +# - description: site-1-nbr-2 +# neighbor_address: 198.51.100.2 +# remote_as: '65562' +# vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 203.0.113.2 +# password: +# encryption: 3 +# key: AF92F4C16A0A0EC5BDF56CF58BC030F6 +# remote_as: '65568' +# vrf: site-2 +# +# commands: +# - router bgp 65563 +# - no bestpath as-path multipath-relax +# - no bestpath compare-neighborid +# - no bestpath cost-community ignore +# - no confederation identifier 42 +# - no log-neighbor-changes +# - no maxas-limit 20 +# - no neighbor-down fib-accelerate +# - no router-id 192.168.1.1 +# - no confederation peers 65020 65030 65040 +# - no neighbor 192.168.1.100 +# - no neighbor 192.168.1.101 +# - no vrf site-1 +# - no vrf site-2 +# +# after: +# as_number: '65563' +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# address-family ipv4 unicast +# default-metric 400 +# suppress-inactive +# default-information originate +# address-family ipv6 multicast +# wait-igp-convergence +# redistribute eigrp eigrp-1 route-map site-1-rmap +# + +# Using purged + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65040 +# bestpath as-path multipath-relax +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 20 +# log-neighbor-changes +# address-family ipv4 unicast +# default-metric 400 +# suppress-inactive +# default-information originate +# address-family ipv6 multicast +# wait-igp-convergence +# redistribute eigrp eigrp-1 route-map site-1-rmap +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# neighbor 192.168.1.101 +# remote-as 65563 +# password 7 12090404011C03162E +# vrf site-1 +# local-as 200 +# log-neighbor-changes +# allocate-index 5000 +# address-family ipv4 multicast +# maximum-paths 40 +# dampen-igp-metric 1200 +# neighbor 198.51.100.1 +# remote-as 65562 +# description site-1-nbr-1 +# password 3 13D4D3549493D2877B1DC116EE27A6BE +# neighbor 198.51.100.2 +# remote-as 65562 +# description site-1-nbr-2 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# remote-as 65568 +# description site-1-nbr-1 +# password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +- name: Purge all BGP configurations from the device + cisco.nxos.nxos_bgp_global: + state: purged + +# Task output +# ------------- + +# before: +# as_number: '65563' +# bestpath: +# as_path: +# multipath_relax: true +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65040' +# log_neighbor_changes: true +# maxas_limit: 20 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# - neighbor_address: 192.168.1.101 +# password: +# encryption: 7 +# key: 12090404011C03162E +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - allocate_index: 5000 +# local_as: '200' +# log_neighbor_changes: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 198.51.100.1 +# password: +# encryption: 3 +# key: 13D4D3549493D2877B1DC116EE27A6BE +# remote_as: '65562' +# - description: site-1-nbr-2 +# neighbor_address: 198.51.100.2 +# remote_as: '65562' +# vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 203.0.113.2 +# password: +# encryption: 3 +# key: AF92F4C16A0A0EC5BDF56CF58BC030F6 +# remote_as: '65568' +# vrf: site-2 +# +# commands: +# - no router bgp 65563 +# +# after: {} +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# Nexus9000v# + +# Using rendered + +- name: Render platform specific configuration lines (without connecting to the device) + cisco.nxos.nxos_bgp_global: + config: + as_number: 65563 + router_id: 192.168.1.1 + bestpath: + as_path: + multipath_relax: True + compare_neighborid: True + cost_community_ignore: True + confederation: + identifier: 42 + peers: + - 65020 + - 65030 + - 65040 + log_neighbor_changes: True + maxas_limit: 20 + neighbors: + - neighbor_address: 192.168.1.100 + neighbor_affinity_group: + group_id: 160 + bmp_activate_server: 1 + remote_as: 65563 + description: NBR-1 + low_memory: + exempt: True + - neighbor_address: 192.168.1.101 + remote_as: 65563 + password: + encryption: 7 + key: 12090404011C03162E + neighbor_down: + fib_accelerate: True + vrfs: + - vrf: site-1 + allocate_index: 5000 + local_as: 200 + log_neighbor_changes: True + neighbors: + - neighbor_address: 198.51.100.1 + description: site-1-nbr-1 + password: + encryption: 3 + key: 13D4D3549493D2877B1DC116EE27A6BE + remote_as: 65562 + - neighbor_address: 198.51.100.2 + remote_as: 65562 + description: site-1-nbr-2 + - vrf: site-2 + local_as: 300 + log_neighbor_changes: True + neighbors: + - neighbor_address: 203.0.113.2 + description: site-1-nbr-1 + password: + encryption: 3 + key: AF92F4C16A0A0EC5BDF56CF58BC030F6 + remote_as: 65568 + neighbor_down: + fib_accelerate: True + +# Task Output (redacted) +# ----------------------- +# rendered: +# - router bgp 65563 +# - bestpath as-path multipath-relax +# - bestpath compare-neighborid +# - bestpath cost-community ignore +# - confederation identifier 42 +# - log-neighbor-changes +# - maxas-limit 20 +# - neighbor-down fib-accelerate +# - router-id 192.168.1.1 +# - confederation peers 65020 65030 65040 +# - neighbor 192.168.1.100 +# - remote-as 65563 +# - affinity-group 160 +# - bmp-activate-server 1 +# - description NBR-1 +# - low-memory exempt +# - neighbor 192.168.1.101 +# - remote-as 65563 +# - password 7 12090404011C03162E +# - vrf site-1 +# - allocate-index 5000 +# - local-as 200 +# - log-neighbor-changes +# - neighbor 198.51.100.1 +# - remote-as 65562 +# - description site-1-nbr-1 +# - password 3 13D4D3549493D2877B1DC116EE27A6BE +# - neighbor 198.51.100.2 +# - remote-as 65562 +# - description site-1-nbr-2 +# - vrf site-2 +# - local-as 300 +# - log-neighbor-changes +# - neighbor-down fib-accelerate +# - neighbor 203.0.113.2 +# - remote-as 65568 +# - description site-1-nbr-1 +# - password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +# Using parsed + +# parsed.cfg +# ------------ +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65040 +# bestpath as-path multipath-relax +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 20 +# log-neighbor-changes +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# neighbor 192.168.1.101 +# remote-as 65563 +# password 7 12090404011C03162E +# vrf site-1 +# local-as 200 +# log-neighbor-changes +# allocate-index 5000 +# neighbor 198.51.100.1 +# remote-as 65562 +# description site-1-nbr-1 +# password 3 13D4D3549493D2877B1DC116EE27A6BE +# neighbor 198.51.100.2 +# remote-as 65562 +# description site-1-nbr-2 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# remote-as 65568 +# description site-1-nbr-1 +# password 3 AF92F4C16A0A0EC5BDF56CF58BC030F6 + +- name: Parse externally provided BGP config + cisco.nxos.nxos_bgp_global: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# as_number: '65563' +# bestpath: +# as_path: +# multipath_relax: true +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65040' +# log_neighbor_changes: true +# maxas_limit: 20 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# - neighbor_address: 192.168.1.101 +# password: +# encryption: 7 +# key: 12090404011C03162E +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - allocate_index: 5000 +# local_as: '200' +# log_neighbor_changes: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 198.51.100.1 +# password: +# encryption: 3 +# key: 13D4D3549493D2877B1DC116EE27A6BE +# remote_as: '65562' +# - description: site-1-nbr-2 +# neighbor_address: 198.51.100.2 +# remote_as: '65562' +# vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - description: site-1-nbr-1 +# neighbor_address: 203.0.113.2 +# password: +# encryption: 3 +# key: AF92F4C16A0A0EC5BDF56CF58BC030F6 +# remote_as: '65568' +# vrf: site-2 + +# Using gathered + +# existing config +# +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65563 +# router-id 192.168.1.1 +# confederation identifier 42 +# confederation peers 65020 65030 65050 +# bestpath cost-community ignore +# bestpath compare-neighborid +# neighbor-down fib-accelerate +# maxas-limit 40 +# neighbor 192.168.1.100 +# low-memory exempt +# bmp-activate-server 1 +# remote-as 65563 +# description NBR-1 +# affinity-group 160 +# vrf site-1 +# vrf site-2 +# local-as 300 +# neighbor-down fib-accelerate +# log-neighbor-changes +# neighbor 203.0.113.2 +# password 7 12090404011C03162E + +- name: Gather BGP facts using gathered + cisco.nxos.nxos_bgp_global: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# as_number: '65563' +# bestpath: +# compare_neighborid: true +# cost_community_ignore: true +# confederation: +# identifier: '42' +# peers: +# - '65020' +# - '65030' +# - '65050' +# maxas_limit: 40 +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - bmp_activate_server: 1 +# description: NBR-1 +# low_memory: +# exempt: true +# neighbor_address: 192.168.1.100 +# neighbor_affinity_group: +# group_id: 160 +# remote_as: '65563' +# router_id: 192.168.1.1 +# vrfs: +# - vrf: site-1 +# - local_as: '300' +# log_neighbor_changes: true +# neighbor_down: +# fib_accelerate: true +# neighbors: +# - neighbor_address: 203.0.113.2 +# password: +# encryption: 7 +# key: 12090404011C03162E +# vrf: site-2 + +# Remove a neighbor having AF configurations with state replaced (will fail) + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# log-neighbor-changes +# maxas-limit 20 +# router-id 198.51.100.2 +# neighbor 203.0.113.2 +# address-family ipv4 unicast +# next-hop-self +# remote-as 65538 +# affinity-group 160 +# description NBR-1 +# low-memory exempt +# neighbor 192.0.2.1 +# remote-as 65537 +# password 7 12090404011C03162E + +- name: Remove a neighbor having AF configurations (should fail) + cisco.nxos.nxos_bgp_global: + config: + as_number: 65536 + router_id: 198.51.100.2 + maxas_limit: 20 + log_neighbor_changes: True + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65537 + password: + encryption: 7 + key: 12090404011C03162E + state: replaced + +# Task output (redacted) +# ----------------------- +# fatal: [Nexus9000v]: FAILED! => changed=false +# msg: Neighbor 203.0.113.2 has address-family configurations. +# Please use the nxos_bgp_neighbor_af module to remove those first. + +# Remove a VRF having AF configurations with state replaced (will fail) + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# log-neighbor-changes +# maxas-limit 20 +# router-id 198.51.100.2 +# neighbor 192.0.2.1 +# remote-as 65537 +# password 7 12090404011C03162E +# vrf site-1 +# address-family ipv4 unicast +# default-information originate +# neighbor 203.0.113.2 +# remote-as 65538 +# affinity-group 160 +# description NBR-1 +# low-memory exempt +# vrf site-2 +# neighbor-down fib-accelerate + +- name: Remove a VRF having AF configurations (should fail) + cisco.nxos.nxos_bgp_global: + config: + as_number: 65536 + router_id: 198.51.100.2 + maxas_limit: 20 + log_neighbor_changes: True + neighbors: + - neighbor_address: 192.0.2.1 + remote_as: 65537 + password: + encryption: 7 + key: 12090404011C03162E + vrfs: + - vrf: site-2 + neighbor_down: + fib_accelerate: True + state: replaced + +# Task output (redacted) +# ----------------------- +# fatal: [Nexus9000v]: FAILED! => changed=false +# msg: VRF site-1 has address-family configurations. +# Please use the nxos_bgp_af module to remove those first. +""" + +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - router bgp 65563 + - maxas-limit 20 + - router-id 192.168.1.1 + - confederation peers 65020 65030 65040 + - neighbor 192.168.1.100 + - remote-as 65563 + - affinity-group 160 + - bmp-activate-server 1 + - description NBR-1 + - low-memory exempt + - vrf site-1 + - log-neighbor-changes + - neighbor 198.51.100.1 + - remote-as 65562 + - description site-1-nbr-1 + - password 3 13D4D3549493D2877B1DC116EE27A6BE +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.bgp_global.bgp_global import ( + Bgp_globalArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.bgp_global.bgp_global import ( + Bgp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Bgp_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Bgp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor.py new file mode 100644 index 00000000..2c5283ca --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor.py @@ -0,0 +1,569 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bgp_neighbor +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2023-01-27) Manages BGP neighbors configurations. +description: +- Manages BGP neighbors configurations on NX-OS switches. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +deprecated: + alternative: nxos_bgp_global + why: Updated module released with more functionality. + removed_at_date: '2023-01-27' +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) removes the whole BGP neighbor configuration. +- Default, where supported, restores params default value. +options: + asn: + description: + - BGP autonomous system number. Valid values are string, Integer in ASPLAIN or + ASDOT notation. + required: true + type: str + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing the global bgp. + default: default + type: str + neighbor: + description: + - Neighbor Identifier. Valid values are string. Neighbors may use IPv4 or IPv6 + notation, with or without prefix length. + required: true + type: str + description: + description: + - Description of the neighbor. + type: str + bfd: + description: + - Enables/Disables BFD for a given neighbor. + - "Dependency: ''feature bfd''" + type: str + choices: + - enable + - disable + connected_check: + description: + - Configure whether or not to check for directly connected peer. + type: bool + capability_negotiation: + description: + - Configure whether or not to negotiate capability with this neighbor. + type: bool + dynamic_capability: + description: + - Configure whether or not to enable dynamic capability. + type: bool + ebgp_multihop: + description: + - Specify multihop TTL for a remote peer. Valid values are integers between 2 + and 255, or keyword 'default' to disable this property. + type: str + local_as: + description: + - Specify the local-as number for the eBGP neighbor. Valid values are String or + Integer in ASPLAIN or ASDOT notation, or 'default', which means not to configure + it. + type: str + log_neighbor_changes: + description: + - Specify whether or not to enable log messages for neighbor up/down event. + choices: + - enable + - disable + - inherit + type: str + low_memory_exempt: + description: + - Specify whether or not to shut down this neighbor under memory pressure. + type: bool + maximum_peers: + description: + - Specify Maximum number of peers for this neighbor prefix Valid values are between + 1 and 1000, or 'default', which does not impose the limit. Note that this parameter + is accepted only on neighbors with address/prefix. + type: str + pwd: + description: + - Specify the password for neighbor. Valid value is string. + type: str + pwd_type: + description: + - Specify the encryption type the password will use. Valid values are '3des' or + 'cisco_type_7' encryption or keyword 'default'. + choices: + - 3des + - cisco_type_7 + - default + type: str + remote_as: + description: + - Specify Autonomous System Number of the neighbor. Valid values are String or + Integer in ASPLAIN or ASDOT notation, or 'default', which means not to configure + it. + type: str + remove_private_as: + description: + - Specify the config to remove private AS number from outbound updates. Valid + values are 'enable' to enable this config, 'disable' to disable this config, + 'all' to remove all private AS number, or 'replace-as', to replace the private + AS number. + choices: + - enable + - disable + - all + - replace-as + type: str + shutdown: + description: + - Configure to administratively shutdown this neighbor. + type: bool + suppress_4_byte_as: + description: + - Configure to suppress 4-byte AS Capability. + type: bool + timers_keepalive: + description: + - Specify keepalive timer value. Valid values are integers between 0 and 3600 + in terms of seconds, or 'default', which is 60. + type: str + timers_holdtime: + description: + - Specify holdtime timer value. Valid values are integers between 0 and 3600 in + terms of seconds, or 'default', which is 180. + type: str + transport_passive_only: + description: + - Specify whether or not to only allow passive connection setup. Valid values + are 'true', 'false', and 'default', which defaults to 'false'. This property + can only be configured when the neighbor is in 'ip' address format without prefix + length. + type: bool + update_source: + description: + - Specify source interface of BGP session and updates. + type: str + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str + peer_type: + description: + - Specify the peer type for BGP session. + choices: + - fabric_border_leaf + - fabric_external + - disable + type: str + version_added: 1.1.0 +""" +EXAMPLES = """ +# create a new neighbor +- cisco.nxos.nxos_bgp_neighbor: + asn: 65535 + neighbor: 192.0.2.3 + local_as: 20 + remote_as: 30 + bfd: enable + description: just a description + update_source: Ethernet1/3 + state: present + peer_type: fabric_external +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "neighbor 192.0.2.3", + "remote-as 30", "update-source Ethernet1/3", + "description just a description", "local-as 20", "peer-type fabric-external"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +BOOL_PARAMS = [ + "capability_negotiation", + "shutdown", + "connected_check", + "dynamic_capability", + "low_memory_exempt", + "suppress_4_byte_as", + "transport_passive_only", +] +PARAM_TO_COMMAND_KEYMAP = { + "asn": "router bgp", + "bfd": "bfd", + "capability_negotiation": "dont-capability-negotiate", + "connected_check": "disable-connected-check", + "description": "description", + "dynamic_capability": "dynamic-capability", + "ebgp_multihop": "ebgp-multihop", + "local_as": "local-as", + "log_neighbor_changes": "log-neighbor-changes", + "low_memory_exempt": "low-memory exempt", + "maximum_peers": "maximum-peers", + "neighbor": "neighbor", + "pwd": "password", + "pwd_type": "password", + "remote_as": "remote-as", + "remove_private_as": "remove-private-as", + "shutdown": "shutdown", + "suppress_4_byte_as": "capability suppress 4-byte-as", + "timers_keepalive": "timers", + "timers_holdtime": "timers", + "transport_passive_only": "transport connection-mode passive", + "update_source": "update-source", + "vrf": "vrf", + "peer_type": "peer-type", +} +PARAM_TO_DEFAULT_KEYMAP = { + "bfd": "disable", + "shutdown": False, + "dynamic_capability": True, + "timers_keepalive": 60, + "timers_holdtime": 180, + "peer_type": "disable", +} + + +def get_value(arg, config): + command = PARAM_TO_COMMAND_KEYMAP[arg] + has_command = re.search(r"^\s+{0}$".format(command), config, re.M) + has_command_val = re.search(r"(?:\s+{0}\s*)(?P<value>.*)$".format(command), config, re.M) + + if arg == "dynamic_capability": + has_no_command = re.search(r"\s+no\s{0}\s*$".format(command), config, re.M) + value = True + if has_no_command: + value = False + elif arg in BOOL_PARAMS: + value = False + if has_command: + value = True + elif arg == "log_neighbor_changes": + value = "" + if has_command: + value = "enable" + elif has_command_val: + value = "disable" + + elif arg == "remove_private_as": + value = "disable" + if has_command: + value = "enable" + elif has_command_val: + value = has_command_val.group("value") + elif arg == "bfd": + value = "enable" if has_command else "disable" + + elif arg == "peer_type": + value = "disable" + if has_command_val: + value = has_command_val.group("value").replace("-", "_") + else: + value = "" + + if has_command_val: + value = has_command_val.group("value") + + if command in ["timers", "password"]: + split_value = value.split() + value = "" + + if arg in ["timers_keepalive", "pwd_type"]: + value = split_value[0] + elif arg in ["timers_holdtime", "pwd"] and len(split_value) == 2: + value = split_value[1] + + return value + + +def get_existing(module, args, warnings): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + + asn_regex = re.compile(r".*router\sbgp\s(?P<existing_asn>\d+(\.\d+)?).*", re.S) + match_asn = asn_regex.match(str(netcfg)) + + if match_asn: + existing_asn = match_asn.group("existing_asn") + parents = ["router bgp {0}".format(existing_asn)] + + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("neighbor {0}".format(module.params["neighbor"])) + config = netcfg.get_section(parents) + if config: + for arg in args: + if arg not in ["asn", "vrf", "neighbor"]: + existing[arg] = get_value(arg, config) + + existing["asn"] = existing_asn + existing["neighbor"] = module.params["neighbor"] + existing["vrf"] = module.params["vrf"] + else: + warnings.append("The BGP process didn't exist but the task just created it.") + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key in table: + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = table.get(key) + + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.items(): + if value is True: + commands.append(key) + elif value is False: + commands.append("no {0}".format(key)) + elif value == "default": + if existing_commands.get(key): + if key == "password": + commands.append("no password") + else: + existing_value = existing_commands.get(key) + commands.append("no {0} {1}".format(key, existing_value)) + else: + if key == "log-neighbor-changes": + if value == "enable": + commands.append("{0}".format(key)) + elif value == "disable": + commands.append("{0} {1}".format(key, value)) + elif value == "inherit": + if existing_commands.get(key): + commands.append("no {0}".format(key)) + elif key == "password": + pwd_type = module.params["pwd_type"] + if pwd_type == "3des": + pwd_type = 3 + else: + pwd_type = 7 + command = "{0} {1} {2}".format(key, pwd_type, value) + if command not in commands: + commands.append(command) + elif key == "remove-private-as": + if value == "enable": + command = "{0}".format(key) + commands.append(command) + elif value == "disable": + if existing_commands.get(key) != "disable": + command = "no {0}".format(key) + commands.append(command) + else: + command = "{0} {1}".format(key, value) + commands.append(command) + elif key == "timers": + if proposed["timers_keepalive"] != PARAM_TO_DEFAULT_KEYMAP.get( + "timers_keepalive", + ) or proposed["timers_holdtime"] != PARAM_TO_DEFAULT_KEYMAP.get("timers_holdtime"): + command = "timers {0} {1}".format( + proposed["timers_keepalive"], + proposed["timers_holdtime"], + ) + if command not in commands: + commands.append(command) + elif key == "bfd": + no_cmd = "no " if value == "disable" else "" + commands.append(no_cmd + key) + elif key == "peer-type": + if value == "disable": + if existing_commands.get(key) != "disable": + command = "no {0}".format(key) + commands.append(command) + elif value == "fabric_external": + ptype = "fabric-external" + command = "{0} {1}".format(key, ptype) + commands.append(command) + elif value == "fabric_border_leaf": + ptype = "fabric-border-leaf" + command = "{0} {1}".format(key, ptype) + commands.append(command) + else: + command = "{0} {1}".format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("neighbor {0}".format(module.params["neighbor"])) + + # make sure that local-as is the last command in the list. + local_as_command = "local-as {0}".format(module.params["local_as"]) + if local_as_command in commands: + commands.remove(local_as_command) + commands.append(local_as_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + commands.append("no neighbor {0}".format(module.params["neighbor"])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type="str"), + vrf=dict(required=False, type="str", default="default"), + neighbor=dict(required=True, type="str"), + description=dict(required=False, type="str"), + bfd=dict(required=False, type="str", choices=["enable", "disable"]), + capability_negotiation=dict(required=False, type="bool"), + connected_check=dict(required=False, type="bool"), + dynamic_capability=dict(required=False, type="bool"), + ebgp_multihop=dict(required=False, type="str"), + local_as=dict(required=False, type="str"), + log_neighbor_changes=dict( + required=False, + type="str", + choices=["enable", "disable", "inherit"], + ), + low_memory_exempt=dict(required=False, type="bool"), + maximum_peers=dict(required=False, type="str"), + pwd=dict(required=False, type="str"), + pwd_type=dict( + required=False, + type="str", + choices=["3des", "cisco_type_7", "default"], + ), + remote_as=dict(required=False, type="str"), + remove_private_as=dict( + required=False, + type="str", + choices=["enable", "disable", "all", "replace-as"], + ), + shutdown=dict(required=False, type="bool"), + suppress_4_byte_as=dict(required=False, type="bool"), + timers_keepalive=dict(required=False, type="str"), + timers_holdtime=dict(required=False, type="str"), + transport_passive_only=dict(required=False, type="bool"), + update_source=dict(required=False, type="str"), + state=dict(choices=["present", "absent"], default="present", required=False), + peer_type=dict( + required=False, + type="str", + choices=["disable", "fabric_border_leaf", "fabric_external"], + ), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=[ + ["timers_holdtime", "timers_keepalive"], + ["pwd", "pwd_type"], + ], + supports_check_mode=True, + ) + + warnings = list() + result = dict(changed=False, warnings=warnings) + + state = module.params["state"] + + if module.params["pwd_type"] == "default": + module.params["pwd_type"] = "0" + + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args, warnings) + + if existing.get("asn") and state == "present": + if existing["asn"] != module.params["asn"]: + module.fail_json( + msg="Another BGP ASN already exists.", + proposed_asn=module.params["asn"], + existing_asn=existing.get("asn"), + ) + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + proposed = {} + for key, value in proposed_args.items(): + if key not in ["asn", "vrf", "neighbor", "pwd_type"]: + if str(value).lower() == "default": + value = PARAM_TO_DEFAULT_KEYMAP.get(key, "default") + if key == "bfd": + if existing.get("bfd", "disable") != value: + proposed[key] = value + elif existing.get(key) != value: + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + if state == "present": + state_present(module, existing, proposed, candidate) + elif state == "absent" and existing: + state_absent(module, existing, proposed, candidate) + + if candidate: + candidate = candidate.items_text() + if not module.check_mode: + load_config(module, candidate) + result["changed"] = True + result["commands"] = candidate + else: + result["commands"] = [] + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_address_family.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_address_family.py new file mode 100644 index 00000000..967f8ee4 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_address_family.py @@ -0,0 +1,1155 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_bgp_neighbor_address_family +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_bgp_neighbor_address_family +short_description: BGP Neighbor Address Family resource module. +description: +- This module manages BGP Neighbor Address Family configuration on devices running Cisco NX-OS. +version_added: 2.0.0 +notes: +- Tested against NX-OS 9.3.6. +- Unsupported for Cisco MDS +- For managing BGP address family configurations please use + the M(cisco.nxos.nxos_bgp_address_family) module. +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^router bgp'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: BGP Neighbor AF configuration. + type: dict + suboptions: + as_number: + description: Autonomous System Number of the router. + type: str + neighbors: &nbr + description: A list of BGP Neighbor AF configuration. + type: list + elements: dict + suboptions: + neighbor_address: + description: IP/IPv6 address of the neighbor. + type: str + required: True + address_family: + description: BGP Neighbor Address Family related configurations. + type: list + elements: dict + suboptions: + afi: + description: Address Family indicator. + type: str + choices: ["ipv4", "ipv6", "link-state", "vpnv4", "vpnv6", "l2vpn"] + required: True + safi: + description: Sub Address Family indicator. + type: str + choices: ["unicast", "multicast", "mvpn", "evpn"] + advertise_map: + description: Specify route-map for conditional advertisement. + type: dict + suboptions: + route_map: + description: Route-map name. + type: str + required: True + exist_map: + description: Condition route-map to advertise only when prefix in condition exists. + type: str + non_exist_map: + description: Condition route-map to advertise only when prefix in condition does not exist. + type: str + advertisement_interval: + description: Minimum interval between sending BGP routing updates. + type: int + allowas_in: + description: Accept as-path with my AS present in it. + type: dict + suboptions: + set: + description: Activate allowas-in property. + type: bool + max_occurences: + description: Number of occurrences of AS number, default is 3. + type: int + as_override: + description: Override matching AS-number while sending update. + type: bool + capability: + description: Advertise capability to the peer. + type: dict + suboptions: + additional_paths: + description: Additional paths capability. + type: dict + suboptions: + receive: + description: Additional paths Receive capability. + type: str + choices: ["enable", "disable"] + send: + description: Additional paths Send capability. + type: str + choices: ["enable", "disable"] + default_originate: + description: Originate a default toward this peer. + type: dict + suboptions: + set: + description: Set default-originate attribute. + type: bool + route_map: + description: Route-map to specify criteria for originating default. + type: str + disable_peer_as_check: + description: Disable checking of peer AS-number while advertising. + type: bool + filter_list: + description: Name of filter-list. + type: dict + suboptions: + inbound: + description: Apply policy to incoming routes. + type: str + outbound: + description: Apply policy to outgoing routes. + type: str + inherit: + description: Inherit a template. + type: dict + suboptions: + template: + description: Template name. + type: str + sequence: + description: Sequence number. + type: int + maximum_prefix: + description: Maximum number of prefixes from this neighbor. + type: dict + suboptions: + max_prefix_limit: + description: Maximum prefix limit. + type: int + generate_warning_threshold: + description: Threshold percentage at which to generate a warning. + type: int + restart_interval: + description: Restart bgp connection after limit is exceeded. + type: int + warning_only: + description: Only give a warning message when limit is exceeded. + type: bool + next_hop_self: + description: Set our address as nexthop (non-reflected). + type: dict + suboptions: + set: + description: Set next-hop-self attribute. + type: bool + all_routes: + description: Set our address as nexthop for all routes. + type: bool + next_hop_third_party: + description: Compute a third-party nexthop if possible. + type: bool + prefix_list: + description: Apply prefix-list. + type: dict + suboptions: + inbound: + description: Apply policy to incoming routes. + type: str + outbound: + description: Apply policy to outgoing routes. + type: str + rewrite_evpn_rt_asn: + description: Auto generate RTs for EBGP neighbor. + type: bool + route_map: + description: Apply route-map to neighbor. + type: dict + suboptions: + inbound: + description: Apply policy to incoming routes. + type: str + outbound: + description: Apply policy to outgoing routes. + type: str + route_reflector_client: + description: Configure a neighbor as Route reflector client. + type: bool + send_community: + description: Send Community attribute to this neighbor. + type: dict + suboptions: + set: + description: Set send-community attribute. + type: bool + extended: + description: Send Extended Community attribute. + type: bool + standard: + description: Send Standard Community attribute. + type: bool + both: + description: Send Standard and Extended Community attributes. + type: bool + soft_reconfiguration_inbound: + description: Soft reconfiguration. + type: dict + suboptions: + set: + description: Set soft-reconfiguration inbound attribute. + type: bool + always: + description: Always perform inbound soft reconfiguration. + type: bool + soo: + description: Specify Site-of-origin extcommunity. + type: str + suppress_inactive: + description: Advertise only active routes to peer. + type: bool + unsuppress_map: + description: Route-map to selectively unsuppress suppressed routes. + type: str + weight: + description: Set default weight for routes from this neighbor. + type: int + vrfs: + description: Virtual Router Context. + type: list + elements: dict + suboptions: + vrf: + description: VRF name. + type: str + neighbors: *nbr + state: + description: + - The state the configuration should be left in. + - State I(deleted) only removes BGP attributes that this modules + manages and does not negate the BGP process completely. + - Refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# Nexus9000v# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_bgp_neighbor_address_family: &id001 + config: + as_number: 65536 + neighbors: + - neighbor_address: 192.0.2.32 + address_family: + - afi: ipv4 + safi: unicast + maximum_prefix: + max_prefix_limit: 20 + generate_warning_threshold: 75 + weight: 100 + prefix_list: + inbound: rmap1 + outbound: rmap2 + - afi: ipv6 + safi: unicast + - neighbor_address: 192.0.2.33 + address_family: + - afi: ipv4 + safi: multicast + inherit: + template: BasePolicy + sequence: 200 + vrfs: + - vrf: site-1 + neighbors: + - neighbor_address: 203.0.113.1 + address_family: + - afi: ipv4 + safi: unicast + suppress_inactive: True + next_hop_self: + set: True + - neighbor_address: 203.0.113.2 + address_family: + - afi: ipv6 + safi: unicast + - afi: ipv4 + safi: multicast + send_community: + set: True + +# Task output +# ------------- +# before: {} +# +# commands: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - address-family ipv4 unicast +# - maximum-prefix 20 75 +# - weight 100 +# - prefix-list rmap1 in +# - prefix-list rmap2 out +# - address-family ipv6 unicast +# - neighbor 192.0.2.33 +# - address-family ipv4 multicast +# - inherit peer-policy BasePolicy 200 +# - vrf site-1 +# - neighbor 203.0.113.1 +# - address-family ipv4 unicast +# - suppress-inactive +# - next-hop-self +# - neighbor 203.0.113.2 +# - address-family ipv6 unicast +# - address-family ipv4 multicast +# - send-community +# +# after: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +# Using replaced + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +- name: Replace specified neighbor AFs with given configuration + cisco.nxos.nxos_bgp_neighbor_address_family: &replaced + config: + as_number: 65536 + neighbors: + - neighbor_address: 192.0.2.32 + address_family: + - afi: ipv4 + safi: unicast + weight: 110 + - afi: ipv6 + safi: unicast + - neighbor_address: 192.0.2.33 + address_family: + - afi: ipv4 + safi: multicast + inherit: + template: BasePolicy + sequence: 200 + vrfs: + - vrf: site-1 + neighbors: + - neighbor_address: 203.0.113.1 + address_family: + - afi: ipv4 + safi: unicast + - neighbor_address: 203.0.113.2 + address_family: + - afi: ipv6 + safi: unicast + - afi: ipv4 + safi: multicast + send_community: + set: True + state: replaced + +# Task output +# ------------- +# before: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast +# +# commands: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - address-family ipv4 unicast +# - no maximum-prefix 20 75 +# - weight 110 +# - no prefix-list rmap1 in +# - no prefix-list rmap2 out +# - vrf site-1 +# - neighbor 203.0.113.1 +# - address-family ipv4 unicast +# - no suppress-inactive +# - no next-hop-self +# +# after: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# weight: 110 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# weight 110 +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +# Using overridden + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +- name: Override all BGP AF configuration with provided configuration + cisco.nxos.nxos_bgp_neighbor_address_family: + config: + as_number: 65536 + neighbors: + - neighbor_address: 192.0.2.32 + address_family: + - afi: ipv4 + safi: unicast + vrfs: + - vrf: site-1 + neighbors: + - neighbor_address: 203.0.113.1 + address_family: + - afi: ipv4 + safi: unicast + suppress_inactive: True + next_hop_self: + set: True + state: overridden + +# Task output +# ------------- +# before: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast +# +# commands: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - address-family ipv4 unicast +# - no maximum-prefix 20 75 +# - no weight 100 +# - no prefix-list rmap1 in +# - no prefix-list rmap2 out +# - no address-family ipv6 unicast +# - neighbor 192.0.2.33 +# - no address-family ipv4 multicast +# - vrf site-1 +# - neighbor 203.0.113.2 +# - no address-family ipv4 multicast +# - no address-family ipv6 unicast +# +# after: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: True +# next_hop_self: +# set: True + +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self + +# Using deleted to remove specified neighbor AFs + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +- name: Delete BGP configs handled by this module + cisco.nxos.nxos_bgp_neighbor_address_family: + config: + as_number: 65536 + neighbors: + - neighbor_address: 192.0.2.32 + address_family: + - afi: ipv4 + safi: unicast + vrfs: + - vrf: site-1 + neighbors: + - neighbor_address: 203.0.113.2 + address_family: + - afi: ipv6 + safi: unicast + state: deleted + +# Task output +# ------------- +# before: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast +# +# commands: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - no address-family ipv4 unicast +# - vrf site-1 +# - neighbor 203.0.113.2 +# - no address-family ipv6 unicast +# +# after: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# + +# Using deleted to remove all neighbor AFs + +# Before state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast +# + +- name: Delete all BGP neighbor AF configs handled by this module + cisco.nxos.nxos_bgp_neighbor_address_family: + state: deleted + +# Task output +# ------------- +# before: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast +# +# commands: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - no address-family ipv4 unicast +# - no address-family ipv6 unicast +# - neighbor 192.0.2.33 +# - no address-family ipv4 multicast +# - vrf site-1 +# - neighbor 203.0.113.1 +# - no address-family ipv4 unicast +# - neighbor 203.0.113.2 +# - no address-family ipv6 unicast +# - no address-family ipv4 multicast +# +# after: +# as_number: "65536" +# +# After state: +# ------------- +# Nexus9000v# show running-config | section "^router bgp" +# router bgp 65536 +# neighbor 192.0.2.32 +# neighbor 192.0.2.33 +# vrf site-1 +# neighbor 203.0.113.1 +# neighbor 203.0.113.2 +# + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_bgp_neighbor_address_family: + config: + as_number: 65536 + neighbors: + - neighbor_address: 192.0.2.32 + address_family: + - afi: ipv4 + safi: unicast + maximum_prefix: + max_prefix_limit: 20 + generate_warning_threshold: 75 + weight: 100 + prefix_list: + inbound: rmap1 + outbound: rmap2 + - afi: ipv6 + safi: unicast + - neighbor_address: 192.0.2.33 + address_family: + - afi: ipv4 + safi: multicast + inherit: + template: BasePolicy + sequence: 200 + vrfs: + - vrf: site-1 + neighbors: + - neighbor_address: 203.0.113.1 + address_family: + - afi: ipv4 + safi: unicast + suppress_inactive: True + next_hop_self: + set: True + - neighbor_address: 203.0.113.2 + address_family: + - afi: ipv6 + safi: unicast + - afi: ipv4 + safi: multicast + send_community: + set: True + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - router bgp 65536 +# - neighbor 192.0.2.32 +# - address-family ipv4 unicast +# - maximum-prefix 20 75 +# - weight 100 +# - prefix-list rmap1 in +# - prefix-list rmap2 out +# - address-family ipv6 unicast +# - neighbor 192.0.2.33 +# - address-family ipv4 multicast +# - inherit peer-policy BasePolicy 200 +# - vrf site-1 +# - neighbor 203.0.113.1 +# - address-family ipv4 unicast +# - suppress-inactive +# - next-hop-self +# - neighbor 203.0.113.2 +# - address-family ipv6 unicast +# - address-family ipv4 multicast +# - send-community + +# Using parsed + +# parsed.cfg +# ------------ +# router bgp 65536 +# neighbor 192.0.2.32 +# address-family ipv4 unicast +# maximum-prefix 20 75 +# weight 100 +# prefix-list rmap1 in +# prefix-list rmap2 out +# address-family ipv6 unicast +# neighbor 192.0.2.33 +# address-family ipv4 multicast +# inherit peer-policy BasePolicy 200 +# vrf site-1 +# neighbor 203.0.113.1 +# address-family ipv4 unicast +# suppress-inactive +# next-hop-self +# neighbor 203.0.113.2 +# address-family ipv4 multicast +# send-community +# address-family ipv6 unicast + +- name: Parse externally provided BGP neighbor AF config + register: result + cisco.nxos.nxos_bgp_neighbor_address_family: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# as_number: "65536" +# neighbors: +# - neighbor_address: 192.0.2.32 +# address_family: +# - afi: ipv4 +# safi: unicast +# maximum_prefix: +# max_prefix_limit: 20 +# generate_warning_threshold: 75 +# weight: 100 +# prefix_list: +# inbound: rmap1 +# outbound: rmap2 +# - afi: ipv6 +# safi: unicast +# - neighbor_address: 192.0.2.33 +# address_family: +# - afi: ipv4 +# safi: multicast +# inherit: +# template: BasePolicy +# sequence: 200 +# vrfs: +# - vrf: site-1 +# neighbors: +# - neighbor_address: 203.0.113.1 +# address_family: +# - afi: ipv4 +# safi: unicast +# suppress_inactive: true +# next_hop_self: +# set: true +# - neighbor_address: 203.0.113.2 +# address_family: +# - afi: ipv4 +# safi: multicast +# send_community: +# set: True +# - afi: ipv6 +# safi: unicast +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.bgp_neighbor_address_family.bgp_neighbor_address_family import ( + Bgp_neighbor_address_familyArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.bgp_neighbor_address_family.bgp_neighbor_address_family import ( + Bgp_neighbor_address_family, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Bgp_neighbor_address_familyArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Bgp_neighbor_address_family(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_af.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_af.py new file mode 100644 index 00000000..cc5624ee --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_bgp_neighbor_af.py @@ -0,0 +1,781 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_bgp_neighbor_af +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2023-02-24) Manages BGP address-family's neighbors configuration. +description: +- Manages BGP address-family's neighbors configurations on NX-OS switches. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +deprecated: + alternative: nxos_bgp_neighbor_address_family + why: Updated module released with more functionality. + removed_at_date: '2023-02-24' +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) removes the whole BGP address-family's neighbor configuration. +- Default, when supported, removes properties +- In order to default maximum-prefix configuration, only C(max_prefix_limit=default) + is needed. +options: + asn: + description: + - BGP autonomous system number. Valid values are String, Integer in ASPLAIN or + ASDOT notation. + required: true + type: str + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing the global bgp. + default: default + type: str + neighbor: + description: + - Neighbor Identifier. Valid values are string. Neighbors may use IPv4 or IPv6 + notation, with or without prefix length. + required: true + type: str + afi: + description: + - Address Family Identifier. + required: true + choices: + - ipv4 + - ipv6 + - vpnv4 + - vpnv6 + - l2vpn + type: str + safi: + description: + - Sub Address Family Identifier. + required: true + choices: + - unicast + - multicast + - evpn + type: str + additional_paths_receive: + description: + - Valid values are enable for basic command enablement; disable for disabling + the command at the neighbor af level (it adds the disable keyword to the basic + command); and inherit to remove the command at this level (the command value + is inherited from a higher BGP layer). + choices: + - enable + - disable + - inherit + type: str + additional_paths_send: + description: + - Valid values are enable for basic command enablement; disable for disabling + the command at the neighbor af level (it adds the disable keyword to the basic + command); and inherit to remove the command at this level (the command value + is inherited from a higher BGP layer). + choices: + - enable + - disable + - inherit + type: str + advertise_map_exist: + description: + - Conditional route advertisement. This property requires two route maps, an advertise-map + and an exist-map. Valid values are an array specifying both the advertise-map + name and the exist-map name, or simply 'default' e.g. ['my_advertise_map', 'my_exist_map']. + This command is mutually exclusive with the advertise_map_non_exist property. + type: list + elements: str + advertise_map_non_exist: + description: + - Conditional route advertisement. This property requires two route maps, an advertise-map + and an exist-map. Valid values are an array specifying both the advertise-map + name and the non-exist-map name, or simply 'default' e.g. ['my_advertise_map', + 'my_non_exist_map']. This command is mutually exclusive with the advertise_map_exist + property. + type: list + elements: str + allowas_in: + description: + - Activate allowas-in property + type: bool + allowas_in_max: + description: + - Max-occurrences value for allowas_in. Valid values are an integer value or 'default'. + This is mutually exclusive with allowas_in. + type: str + as_override: + description: + - Activate the as-override feature. + type: bool + default_originate: + description: + - Activate the default-originate feature. + type: bool + default_originate_route_map: + description: + - Route-map for the default_originate property. Valid values are a string defining + a route-map name, or 'default'. This is mutually exclusive with default_originate. + type: str + disable_peer_as_check: + description: + - Disable checking of peer AS-number while advertising + type: bool + filter_list_in: + description: + - Valid values are a string defining a filter-list name, or 'default'. + type: str + filter_list_out: + description: + - Valid values are a string defining a filter-list name, or 'default'. + type: str + max_prefix_limit: + description: + - maximum-prefix limit value. Valid values are an integer value or 'default'. + type: str + max_prefix_interval: + description: + - Optional restart interval. Valid values are an integer. Requires max_prefix_limit. + May not be combined with max_prefix_warning. + type: str + max_prefix_threshold: + description: + - Optional threshold percentage at which to generate a warning. Valid values are + an integer value. Requires max_prefix_limit. + type: str + max_prefix_warning: + description: + - Optional warning-only keyword. Requires max_prefix_limit. May not be combined + with max_prefix_interval. + type: bool + next_hop_self: + description: + - Activate the next-hop-self feature. + type: bool + next_hop_third_party: + description: + - Activate the next-hop-third-party feature. + type: bool + prefix_list_in: + description: + - Valid values are a string defining a prefix-list name, or 'default'. + type: str + prefix_list_out: + description: + - Valid values are a string defining a prefix-list name, or 'default'. + type: str + route_map_in: + description: + - Valid values are a string defining a route-map name, or 'default'. + type: str + route_map_out: + description: + - Valid values are a string defining a route-map name, or 'default'. + type: str + route_reflector_client: + description: + - Router reflector client. + type: bool + send_community: + description: + - send-community attribute. + choices: + - none + - both + - extended + - standard + - default + type: str + soft_reconfiguration_in: + description: + - Valid values are 'enable' for basic command enablement; 'always' to add the + always keyword to the basic command; and 'inherit' to remove the command at + this level (the command value is inherited from a higher BGP layer). + choices: + - enable + - always + - inherit + type: str + soo: + description: + - Site-of-origin. Valid values are a string defining a VPN extcommunity or 'default'. + type: str + suppress_inactive: + description: + - suppress-inactive feature. + type: bool + unsuppress_map: + description: + - unsuppress-map. Valid values are a string defining a route-map name or 'default'. + type: str + weight: + description: + - Weight value. Valid values are an integer value or 'default'. + type: str + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str + rewrite_evpn_rt_asn: + description: + - Auto generate route targets for EBGP neighbor. + type: bool + version_added: 1.1.0 +""" +EXAMPLES = """ +- name: configure RR client + cisco.nxos.nxos_bgp_neighbor_af: + asn: 65535 + neighbor: 192.0.2.3 + afi: ipv4 + safi: unicast + route_reflector_client: true + state: present + rewrite_evpn_rt_asn: true +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "neighbor 192.0.2.3", + "address-family ipv4 unicast", "route-reflector-client", + "rewrite-evpn-rt-asn"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +BOOL_PARAMS = [ + "allowas_in", + "as_override", + "default_originate", + "disable_peer_as_check", + "next_hop_self", + "next_hop_third_party", + "route_reflector_client", + "suppress_inactive", + "rewrite_evpn_rt_asn", +] +PARAM_TO_COMMAND_KEYMAP = { + "afi": "address-family", + "asn": "router bgp", + "neighbor": "neighbor", + "additional_paths_receive": "capability additional-paths receive", + "additional_paths_send": "capability additional-paths send", + "advertise_map_exist": "advertise-map exist-map", + "advertise_map_non_exist": "advertise-map non-exist-map", + "allowas_in": "allowas-in", + "allowas_in_max": "allowas-in", + "as_override": "as-override", + "default_originate": "default-originate", + "default_originate_route_map": "default-originate route-map", + "disable_peer_as_check": "disable-peer-as-check", + "filter_list_in": "filter-list in", + "filter_list_out": "filter-list out", + "max_prefix_limit": "maximum-prefix", + "max_prefix_interval": "maximum-prefix interval", + "max_prefix_threshold": "maximum-prefix threshold", + "max_prefix_warning": "maximum-prefix warning", + "next_hop_self": "next-hop-self", + "next_hop_third_party": "next-hop-third-party", + "prefix_list_in": "prefix-list in", + "prefix_list_out": "prefix-list out", + "route_map_in": "route-map in", + "route_map_out": "route-map out", + "route_reflector_client": "route-reflector-client", + "safi": "address-family", + "send_community": "send-community", + "soft_reconfiguration_in": "soft-reconfiguration inbound", + "soo": "soo", + "suppress_inactive": "suppress-inactive", + "unsuppress_map": "unsuppress-map", + "weight": "weight", + "vrf": "vrf", + "rewrite_evpn_rt_asn": "rewrite-evpn-rt-asn", +} + + +def get_value(arg, config, module): + custom = [ + "additional_paths_send", + "additional_paths_receive", + "max_prefix_limit", + "max_prefix_interval", + "max_prefix_threshold", + "max_prefix_warning", + "send_community", + "soft_reconfiguration_in", + ] + command = PARAM_TO_COMMAND_KEYMAP[arg] + has_command = re.search(r"^\s+{0}\s*".format(command), config, re.M) + has_command_val = re.search(r"(?:{0}\s)(?P<value>.*)$".format(command), config, re.M) + value = "" + + if arg in custom: + value = get_custom_value(arg, config, module) + + elif arg == "next_hop_third_party": + has_no_command = re.search(r"^\s+no\s+{0}\s*$".format(command), config, re.M) + value = False + if not has_no_command: + value = True + + elif arg in BOOL_PARAMS: + value = False + if has_command: + value = True + + elif command.startswith("advertise-map"): + value = [] + has_adv_map = re.search( + r"{0}\s(?P<value1>.*)\s{1}\s(?P<value2>.*)$".format(*command.split()), + config, + re.M, + ) + if has_adv_map: + value = list(has_adv_map.groups()) + + elif command.split()[0] in ["filter-list", "prefix-list", "route-map"]: + has_cmd_direction_val = re.search( + r"{0}\s(?P<value>.*)\s{1}$".format(*command.split()), + config, + re.M, + ) + if has_cmd_direction_val: + value = has_cmd_direction_val.group("value") + + elif has_command_val: + value = has_command_val.group("value") + + return value + + +def get_custom_value(arg, config, module): + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + splitted_config = config.splitlines() + value = "" + + if arg.startswith("additional_paths"): + value = "inherit" + for line in splitted_config: + if command in line: + if "disable" in line: + value = "disable" + else: + value = "enable" + elif arg.startswith("max_prefix"): + for line in splitted_config: + if "maximum-prefix" in line: + splitted_line = line.split() + if arg == "max_prefix_limit": + value = splitted_line[1] + elif arg == "max_prefix_interval" and "restart" in line: + value = splitted_line[-1] + elif arg == "max_prefix_threshold" and len(splitted_line) > 2: + try: + int(splitted_line[2]) + value = splitted_line[2] + except ValueError: + value = "" + elif arg == "max_prefix_warning": + value = "warning-only" in line + elif arg == "soft_reconfiguration_in": + value = "inherit" + for line in splitted_config: + if command in line: + if "always" in line: + value = "always" + else: + value = "enable" + + elif arg == "send_community": + value = "none" + for line in splitted_config: + if command in line: + if "extended" in line: + if value == "standard": + value = "both" + else: + value = "extended" + elif "both" in line: + value = "both" + else: + value = "standard" + + return value + + +def get_existing(module, args, warnings): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + + asn_regex = re.compile(r".*router\sbgp\s(?P<existing_asn>\d+(\.\d+)?).*", re.S) + match_asn = asn_regex.match(str(netcfg)) + + if match_asn: + existing_asn = match_asn.group("existing_asn") + parents = ["router bgp {0}".format(existing_asn)] + + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("neighbor {0}".format(module.params["neighbor"])) + parents.append("address-family {0} {1}".format(module.params["afi"], module.params["safi"])) + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg not in ["asn", "vrf", "neighbor", "afi", "safi"]: + existing[arg] = get_value(arg, config, module) + + existing["asn"] = existing_asn + existing["neighbor"] = module.params["neighbor"] + existing["vrf"] = module.params["vrf"] + existing["afi"] = module.params["afi"] + existing["safi"] = module.params["safi"] + else: + warnings.append("The BGP process didn't exist but the task just created it.") + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key in table: + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = table.get(key) + + return new_dict + + +def get_default_command(key, value, existing_commands): + command = "" + if existing_commands.get(key): + existing_value = existing_commands.get(key) + if value == "inherit": + if existing_value != "inherit": + command = "no {0}".format(key) + else: + if key == "advertise-map exist-map": + command = "no advertise-map {0} exist-map {1}".format( + existing_value[0], + existing_value[1], + ) + elif key == "advertise-map non-exist-map": + command = "no advertise-map {0} non-exist-map {1}".format( + existing_value[0], + existing_value[1], + ) + elif key == "filter-list in": + command = "no filter-list {0} in".format(existing_value) + elif key == "filter-list out": + command = "no filter-list {0} out".format(existing_value) + elif key == "prefix-list in": + command = "no prefix-list {0} in".format(existing_value) + elif key == "prefix-list out": + command = "no prefix-list {0} out".format(existing_value) + elif key == "route-map in": + command = "no route-map {0} in".format(existing_value) + elif key == "route-map out": + command = "no route-map {0} out".format(existing_value) + elif key.startswith("maximum-prefix"): + command = "no maximum-prefix" + elif key == "allowas-in max": + command = ["no allowas-in {0}".format(existing_value)] + command.append("allowas-in") + else: + command = "no {0} {1}".format(key, existing_value) + else: + if key.replace(" ", "_").replace("-", "_") in BOOL_PARAMS: + command = "no {0}".format(key) + return command + + +def fix_proposed(module, existing, proposed): + allowas_in = proposed.get("allowas_in") + allowas_in_max = proposed.get("allowas_in_max") + + if allowas_in_max and not allowas_in: + proposed.pop("allowas_in_max") + elif allowas_in and allowas_in_max: + proposed.pop("allowas_in") + + if existing.get("send_community") == "none" and proposed.get("send_community") == "default": + proposed.pop("send_community") + return proposed + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed = fix_proposed(module, existing, proposed) + + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.items(): + if value in ["inherit", "default"]: + command = get_default_command(key, value, existing_commands) + + if isinstance(command, str): + if command and command not in commands: + commands.append(command) + elif isinstance(command, list): + for cmd in command: + if cmd not in commands: + commands.append(cmd) + + elif key.startswith("maximum-prefix"): + if module.params["max_prefix_limit"] != "default": + command = "maximum-prefix {0}".format(module.params["max_prefix_limit"]) + if module.params["max_prefix_threshold"]: + command += " {0}".format(module.params["max_prefix_threshold"]) + if module.params["max_prefix_interval"]: + command += " restart {0}".format(module.params["max_prefix_interval"]) + elif module.params["max_prefix_warning"]: + command += " warning-only" + commands.append(command) + + elif value is True: + commands.append(key) + elif value is False: + commands.append("no {0}".format(key)) + elif key == "address-family": + commands.append( + "address-family {0} {1}".format(module.params["afi"], module.params["safi"]), + ) + elif key.startswith("capability additional-paths"): + command = key + if value == "disable": + command += " disable" + commands.append(command) + elif key.startswith("advertise-map"): + direction = key.split()[1] + commands.append("advertise-map {1} {0} {2}".format(direction, *value)) + elif key.split()[0] in ["filter-list", "prefix-list", "route-map"]: + commands.append("{1} {0} {2}".format(value, *key.split())) + + elif key == "soft-reconfiguration inbound": + command = "" + if value == "enable": + command = key + elif value == "always": + command = "{0} {1}".format(key, value) + commands.append(command) + elif key == "send-community": + command = key + if value in ["standard", "extended"]: + commands.append("no " + key + " both") + command += " {0}".format(value) + commands.append(command) + else: + command = "{0} {1}".format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("neighbor {0}".format(module.params["neighbor"])) + + af_command = "address-family {0} {1}".format(module.params["afi"], module.params["safi"]) + parents.append(af_command) + if af_command in commands: + commands.remove(af_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params["asn"])] + if module.params["vrf"] != "default": + parents.append("vrf {0}".format(module.params["vrf"])) + + parents.append("neighbor {0}".format(module.params["neighbor"])) + commands.append("no address-family {0} {1}".format(module.params["afi"], module.params["safi"])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type="str"), + vrf=dict(required=False, type="str", default="default"), + neighbor=dict(required=True, type="str"), + afi=dict( + required=True, + type="str", + choices=["ipv4", "ipv6", "vpnv4", "vpnv6", "l2vpn"], + ), + safi=dict(required=True, type="str", choices=["unicast", "multicast", "evpn"]), + additional_paths_receive=dict( + required=False, + type="str", + choices=["enable", "disable", "inherit"], + ), + additional_paths_send=dict( + required=False, + type="str", + choices=["enable", "disable", "inherit"], + ), + advertise_map_exist=dict(required=False, type="list", elements="str"), + advertise_map_non_exist=dict(required=False, type="list", elements="str"), + allowas_in=dict(required=False, type="bool"), + allowas_in_max=dict(required=False, type="str"), + as_override=dict(required=False, type="bool"), + default_originate=dict(required=False, type="bool"), + default_originate_route_map=dict(required=False, type="str"), + disable_peer_as_check=dict(required=False, type="bool"), + filter_list_in=dict(required=False, type="str"), + filter_list_out=dict(required=False, type="str"), + max_prefix_limit=dict(required=False, type="str"), + max_prefix_interval=dict(required=False, type="str"), + max_prefix_threshold=dict(required=False, type="str"), + max_prefix_warning=dict(required=False, type="bool"), + next_hop_self=dict(required=False, type="bool"), + next_hop_third_party=dict(required=False, type="bool"), + prefix_list_in=dict(required=False, type="str"), + prefix_list_out=dict(required=False, type="str"), + route_map_in=dict(required=False, type="str"), + route_map_out=dict(required=False, type="str"), + route_reflector_client=dict(required=False, type="bool"), + send_community=dict( + required=False, + choices=["none", "both", "extended", "standard", "default"], + ), + soft_reconfiguration_in=dict( + required=False, + type="str", + choices=["enable", "always", "inherit"], + ), + soo=dict(required=False, type="str"), + suppress_inactive=dict(required=False, type="bool"), + unsuppress_map=dict(required=False, type="str"), + weight=dict(required=False, type="str"), + state=dict(choices=["present", "absent"], default="present", required=False), + rewrite_evpn_rt_asn=dict(required=False, type="bool"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ["advertise_map_exist", "advertise_map_non_exist"], + ["max_prefix_interval", "max_prefix_warning"], + ["default_originate", "default_originate_route_map"], + ["allowas_in", "allowas_in_max"], + ], + supports_check_mode=True, + ) + + warnings = list() + result = dict(changed=False, warnings=warnings) + + state = module.params["state"] + for key in [ + "max_prefix_interval", + "max_prefix_warning", + "max_prefix_threshold", + ]: + if module.params[key] and not module.params["max_prefix_limit"]: + module.fail_json(msg="max_prefix_limit is required when using %s" % key) + if module.params["vrf"] == "default" and module.params["soo"]: + module.fail_json(msg="SOO is only allowed in non-default VRF") + + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args, warnings) + + if existing.get("asn") and state == "present": + if existing.get("asn") != module.params["asn"]: + module.fail_json( + msg="Another BGP ASN already exists.", + proposed_asn=module.params["asn"], + existing_asn=existing.get("asn"), + ) + + for param in ["advertise_map_exist", "advertise_map_non_exist"]: + if module.params[param] == ["default"]: + module.params[param] = "default" + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.items(): + if key not in ["asn", "vrf", "neighbor"]: + if not isinstance(value, list): + if str(value).lower() == "true": + value = True + elif str(value).lower() == "false": + value = False + elif str(value).lower() == "default": + if key in BOOL_PARAMS: + value = False + else: + value = "default" + elif key == "send_community" and str(value).lower() == "none": + value = "default" + if existing.get(key) != value: + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + if state == "present": + state_present(module, existing, proposed, candidate) + elif state == "absent" and existing: + state_absent(module, existing, candidate) + + if candidate: + candidate = candidate.items_text() + if not module.check_mode: + responses = load_config(module, candidate) + if responses: + for resp in responses: + if resp: + if resp.endswith("is valid only for EBGP peers"): + module.fail_json(msg=resp) + result["changed"] = True + result["commands"] = candidate + else: + result["commands"] = [] + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_command.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_command.py new file mode 100644 index 00000000..7febbf8a --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_command.py @@ -0,0 +1,231 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 = """ +module: nxos_command +extends_documentation_fragment: +- cisco.nxos.nxos +author: Peter Sprygada (@privateip) +notes: +- Limited Support for Cisco MDS +short_description: Run arbitrary command on Cisco NXOS devices +description: +- Sends an arbitrary command to an NXOS node and returns the results read from the + device. This module includes an argument that will cause the module to wait for + a specific condition before returning or timing out if the condition is not met. +version_added: 1.0.0 +options: + commands: + description: + - The commands to send to the remote NXOS device. The resulting output from the + command is returned. If the I(wait_for) argument is provided, the module is + not returned until the condition is satisfied or the number of retires as expired. + - The I(commands) argument also accepts an alternative form that allows for complex + values that specify the command to run and the output format to return. This + can be done on a command by command basis. The complex argument supports the + keywords C(command) and C(output) where C(command) is the command to run and + C(output) is one of 'text' or 'json'. + - If a command sent to the device requires answering a prompt, it is possible to pass + a dict containing command, answer and prompt. Common answers are 'y' or "\\r" + (carriage return, must be double quotes). See examples. + required: true + type: list + elements: raw + wait_for: + description: + - Specifies what to evaluate from the output of the command and what conditionals + to apply. This argument will cause the task to wait for a particular conditional + to be true before moving forward. If the conditional is not true by the configured + retries, the task fails. See examples. + aliases: + - waitfor + type: list + elements: str + match: + description: + - The I(match) argument is used in conjunction with the I(wait_for) argument to + specify the match policy. Valid values are C(all) or C(any). If the value + is set to C(all) then all conditionals in the I(wait_for) must be satisfied. If + the value is set to C(any) then only one of the values must be satisfied. + default: all + choices: ['any', 'all'] + type: str + retries: + description: + - Specifies the number of retries a command should by tried before it is considered + failed. The command is run on the target device every retry and evaluated against + the I(wait_for) conditionals. + - The commands are run once when I(retries) is set to C(0). + default: 9 + type: int + interval: + description: + - Configures the interval in seconds to wait between retries of the command. If + the command does not pass the specified conditional, the interval indicates + how to long to wait before trying the command again. + default: 1 + type: int +""" + +EXAMPLES = """ +- name: run show version on remote devices + cisco.nxos.nxos_command: + commands: show version + +- name: run show version and check to see if output contains Cisco + cisco.nxos.nxos_command: + commands: show version + wait_for: result[0] contains Cisco + +- name: run multiple commands on remote nodes + cisco.nxos.nxos_command: + commands: + - show version + - show interfaces + +- name: run multiple commands and evaluate the output + cisco.nxos.nxos_command: + commands: + - show version + - show interfaces + wait_for: + - result[0] contains Cisco + - result[1] contains loopback0 + +- name: run commands and specify the output format + cisco.nxos.nxos_command: + commands: + - command: show version + output: json + +- name: run commands that require answering a prompt + cisco.nxos.nxos_command: + commands: + - configure terminal + - command: no feature npv + prompt: Do you want to continue + answer: y + +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always apart from low level errors (such as action plugin) + type: list + sample: ['...', '...'] +stdout_lines: + description: The value of stdout split into a list + returned: always apart from low level errors (such as action plugin) + type: list + sample: [['...', '...'], ['...'], ['...']] +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] +""" +import time + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.parsing import ( + Conditional, + FailedConditionalError, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + to_lines, + transform_commands, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import run_commands + + +def parse_commands(module, warnings): + commands = transform_commands(module) + + if module.check_mode: + for item in list(commands): + if not item["command"].startswith("show"): + warnings.append( + "Only show commands are supported when using check mode, not " + "executing %s" % item["command"], + ) + commands.remove(item) + + return commands + + +def to_cli(obj): + cmd = obj["command"] + if obj.get("output") == "json": + cmd += " | json" + return cmd + + +def main(): + """entry point for module execution""" + argument_spec = dict( + # { command: <str>, output: <str>, prompt: <str>, response: <str> } + commands=dict(type="list", required=True, elements="raw"), + wait_for=dict(type="list", aliases=["waitfor"], elements="str"), + match=dict(default="all", choices=["any", "all"]), + retries=dict(default=9, type="int"), + interval=dict(default=1, type="int"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + result = {"changed": False, "warnings": warnings} + commands = parse_commands(module, warnings) + wait_for = module.params["wait_for"] or list() + conditionals = [] + + try: + conditionals = [Conditional(c) for c in wait_for] + except AttributeError as exc: + module.fail_json(msg=to_text(exc)) + + retries = module.params["retries"] + interval = module.params["interval"] + match = module.params["match"] + + while retries >= 0: + responses = run_commands(module, commands) + + for item in list(conditionals): + try: + if item(responses): + if match == "any": + conditionals = list() + break + conditionals.remove(item) + except FailedConditionalError as exc: + module.fail_json(msg=to_text(exc)) + + if not conditionals: + break + + time.sleep(interval) + retries -= 1 + + if conditionals: + failed_conditions = [item.raw for item in conditionals] + msg = "One or more conditional statements have not been satisfied" + module.fail_json(msg=msg, failed_conditions=failed_conditions) + + result.update({"stdout": responses, "stdout_lines": list(to_lines(responses))}) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_config.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_config.py new file mode 100644 index 00000000..45789124 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_config.py @@ -0,0 +1,579 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_config +extends_documentation_fragment: +- cisco.nxos.nxos +author: Peter Sprygada (@privateip) +short_description: Manage Cisco NXOS configuration sections +description: +- Cisco NXOS configurations use a simple block indent file syntax for segmenting configuration + into sections. This module provides an implementation for working with NXOS configuration + sections in a deterministic way. This module works with either CLI or NXAPI transports. +version_added: 1.0.0 +options: + lines: + description: + - The ordered set of commands that should be configured in the section. The commands + must be the exact same commands as found in the device running-config to ensure idempotency + and correct diff. Be sure to note the configuration command syntax as some commands are + automatically modified by the device config parser. + type: list + aliases: + - commands + elements: str + parents: + description: + - The ordered set of parents that uniquely identify the section or hierarchy the + commands should be checked against. If the parents argument is omitted, the + commands are checked against the set of top level or global commands. + type: list + elements: str + src: + description: + - The I(src) argument provides a path to the configuration file to load into the + remote system. The path can either be a full system path to the configuration + file if the value starts with / or relative to the root of the implemented role + or playbook. This argument is mutually exclusive with the I(lines) and I(parents) + arguments. The configuration lines in the source file should be similar to how it + will appear if present in the running-configuration of the device including indentation + to ensure idempotency and correct diff. + type: path + replace_src: + description: + - The I(replace_src) argument provides path to the configuration file to load + into the remote system. This argument is used to replace the entire config with + a flat-file. This is used with argument I(replace) with value I(config). This + is mutually exclusive with the I(lines) and I(src) arguments. This argument + will only work for NX-OS versions that support `config replace`. Use I(nxos_file_copy) + module to copy the flat file to remote device and then use the path with this argument. + The configuration lines in the file should be similar to how it + will appear if present in the running-configuration of the device including the indentation + to ensure idempotency and correct diff. + type: str + before: + description: + - The ordered set of commands to push on to the command stack if a change needs + to be made. This allows the playbook designer the opportunity to perform configuration + commands prior to pushing any changes without affecting how the set of commands + are matched against the system. + type: list + elements: str + after: + description: + - The ordered set of commands to append to the end of the command stack if a change + needs to be made. Just like with I(before) this allows the playbook designer + to append a set of commands to be executed after the command set. + type: list + elements: str + match: + description: + - Instructs the module on the way to perform the matching of the set of commands + against the current device config. If match is set to I(line), commands are + matched line by line. If match is set to I(strict), command lines are matched + with respect to position. If match is set to I(exact), command lines must be + an equal match. Finally, if match is set to I(none), the module will not attempt + to compare the source configuration with the running configuration on the remote + device. + default: line + choices: + - line + - strict + - exact + - none + type: str + replace: + description: + - Instructs the module on the way to perform the configuration on the device. If + the replace argument is set to I(line) then the modified lines are pushed to + the device in configuration mode. If the replace argument is set to I(block) + then the entire command block is pushed to the device in configuration mode + if any line is not correct. replace I(config) will only work for NX-OS versions + that support `config replace`. + default: line + choices: + - line + - block + - config + type: str + backup: + description: + - This argument will cause the module to create a full backup of the current C(running-config) + from the remote device before any changes are made. If the C(backup_options) + value is not given, the backup file is written to the C(backup) folder in the + playbook root directory or role root directory, if playbook is part of an ansible + role. If the directory does not exist, it is created. + type: bool + default: no + running_config: + description: + - The module, by default, will connect to the remote device and retrieve the current + running-config to use as a base for comparing against the contents of source. There + are times when it is not desirable to have the task get the current running-config + for every task in a playbook. The I(running_config) argument allows the implementer + to pass in the configuration to use as the base config for comparison. + The configuration lines for this option should be similar to how it will appear if present + in the running-configuration of the device including the indentation to ensure idempotency + and correct diff. + aliases: + - config + type: str + defaults: + description: + - The I(defaults) argument will influence how the running-config is collected + from the device. When the value is set to true, the command used to collect + the running-config is append with the all keyword. When the value is set to + false, the command is issued without the all keyword + type: bool + default: no + save_when: + description: + - When changes are made to the device running-configuration, the changes are not + copied to non-volatile storage by default. Using this argument will change + that before. If the argument is set to I(always), then the running-config will + always be copied to the startup-config and the I(modified) flag will always + be set to True. If the argument is set to I(modified), then the running-config + will only be copied to the startup-config if it has changed since the last save + to startup-config. If the argument is set to I(never), the running-config will + never be copied to the startup-config. If the argument is set to I(changed), + then the running-config will only be copied to the startup-config if the task + has made a change. I(changed) was added in Ansible 2.6. + default: never + choices: + - always + - never + - modified + - changed + type: str + diff_against: + description: + - When using the C(ansible-playbook --diff) command line argument the module can + generate diffs against different sources. + - When this option is configure as I(startup), the module will return the diff + of the running-config against the startup-config. + - When this option is configured as I(intended), the module will return the diff + of the running-config against the configuration provided in the C(intended_config) + argument. + - When this option is configured as I(running), the module will return the before + and after diff of the running-config with respect to any changes made to the + device configuration. + choices: + - startup + - intended + - running + type: str + diff_ignore_lines: + description: + - Use this argument to specify one or more lines that should be ignored during + the diff. This is used for lines in the configuration that are automatically + updated by the system. This argument takes a list of regular expressions or + exact line matches. + type: list + elements: str + intended_config: + description: + - The C(intended_config) provides the master configuration that the node should + conform to and is used to check the final running-config against. This argument + will not modify any settings on the remote device and is strictly used to check + the compliance of the current device's configuration against. When specifying + this argument, the task should also modify the C(diff_against) value and set + it to I(intended). The configuration lines for this value should be similar to how it + will appear if present in the running-configuration of the device including the indentation + to ensure correct diff. + type: str + backup_options: + description: + - This is a dict object containing configurable options related to backup file + path. The value of this option is read only when C(backup) is set to I(True), + if C(backup) is set to I(false) this option will be silently ignored. + suboptions: + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by <hostname>_config.<current-date>@<current-time> + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be created and the filename is either the value of C(filename) or default + filename as described in C(filename) options description. If the path value + is not given in that case a I(backup) directory will be created in the current + working directory and backup configuration will be copied in C(filename) + within I(backup) directory. + type: path + type: dict +notes: +- Unsupported for Cisco MDS +- Abbreviated commands are NOT idempotent, see + U(https://docs.ansible.com/ansible/latest/network/user_guide/faq.html#why-do-the-config-modules-always-return-changed-true-with-abbreviated-commands). +- To ensure idempotency and correct diff the configuration lines in the relevant module options should be similar to how they + appear if present in the running configuration on device including the indentation. +""" + +EXAMPLES = """ +- name: configure top level configuration and save it + cisco.nxos.nxos_config: + lines: hostname {{ inventory_hostname }} + save_when: modified + +- name: diff the running-config against a provided config + cisco.nxos.nxos_config: + diff_against: intended + intended_config: "{{ lookup('file', 'master.cfg') }}" + +- cisco.nxos.nxos_config: + lines: + - 10 permit ip 192.0.2.1/32 any log + - 20 permit ip 192.0.2.2/32 any log + - 30 permit ip 192.0.2.3/32 any log + - 40 permit ip 192.0.2.4/32 any log + - 50 permit ip 192.0.2.5/32 any log + parents: ip access-list test + before: no ip access-list test + match: exact + +- cisco.nxos.nxos_config: + lines: + - 10 permit ip 192.0.2.1/32 any log + - 20 permit ip 192.0.2.2/32 any log + - 30 permit ip 192.0.2.3/32 any log + - 40 permit ip 192.0.2.4/32 any log + parents: ip access-list test + before: no ip access-list test + replace: block + +- name: replace config with flat file + cisco.nxos.nxos_config: + replace_src: config.txt + replace: config + +- name: for idempotency, use full-form commands + cisco.nxos.nxos_config: + lines: + # - shut + - shutdown + # parents: int eth1/1 + parents: interface Ethernet1/1 + +- name: configurable backup path + cisco.nxos.nxos_config: + backup: yes + backup_options: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +commands: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['hostname foo', 'vlan 1', 'name default'] +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['hostname foo', 'vlan 1', 'name default'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: str + sample: /playbooks/ansible/backup/nxos_config.2016-07-16@22:28:34 +filename: + description: The name of the backup file + returned: when backup is yes and filename is not specified in backup options + type: str + sample: nxos_config.2016-07-16@22:28:34 +shortname: + description: The full path to the backup file excluding the timestamp + returned: when backup is yes and filename is not specified in backup options + type: str + sample: /playbooks/ansible/backup/nxos_config +date: + description: The date extracted from the backup file name + returned: when backup is yes + type: str + sample: "2016-07-16" +time: + description: The time extracted from the backup file name + returned: when backup is yes + type: str + sample: "22:28:34" +""" +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, + dumps, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + get_connection, + load_config, + run_commands, +) + + +def get_running_config(module, config=None, flags=None): + contents = module.params["running_config"] + if not contents: + if config: + contents = config + else: + contents = get_config(module, flags=flags) + return contents + + +def get_candidate(module): + candidate = "" + if module.params["src"]: + if module.params["replace"] != "config": + candidate = module.params["src"] + if module.params["replace"] == "config": + candidate = "config replace {0}".format(module.params["replace_src"]) + elif module.params["lines"]: + candidate_obj = NetworkConfig(indent=2) + parents = module.params["parents"] or list() + candidate_obj.add(module.params["lines"], parents=parents) + candidate = dumps(candidate_obj, "raw") + return candidate + + +def execute_show_commands(module, commands, output="text"): + cmds = [] + for command in to_list(commands): + cmd = {"command": command, "output": output} + cmds.append(cmd) + body = run_commands(module, cmds) + return body + + +def save_config(module, result): + result["changed"] = True + if not module.check_mode: + cmd = { + "command": "copy running-config startup-config", + "output": "text", + } + run_commands(module, [cmd]) + else: + module.warn( + "Skipping command `copy running-config startup-config` " + "due to check_mode. Configuration not copied to " + "non-volatile storage", + ) + + +def main(): + """main entry point for module execution""" + backup_spec = dict(filename=dict(), dir_path=dict(type="path")) + argument_spec = dict( + src=dict(type="path"), + replace_src=dict(), + lines=dict(aliases=["commands"], type="list", elements="str"), + parents=dict(type="list", elements="str"), + before=dict(type="list", elements="str"), + after=dict(type="list", elements="str"), + match=dict(default="line", choices=["line", "strict", "exact", "none"]), + replace=dict(default="line", choices=["line", "block", "config"]), + running_config=dict(aliases=["config"]), + intended_config=dict(), + defaults=dict(type="bool", default=False), + backup=dict(type="bool", default=False), + backup_options=dict(type="dict", options=backup_spec), + save_when=dict(choices=["always", "never", "modified", "changed"], default="never"), + diff_against=dict(choices=["running", "startup", "intended"]), + diff_ignore_lines=dict(type="list", elements="str"), + ) + + mutually_exclusive = [("lines", "src", "replace_src"), ("parents", "src")] + + required_if = [ + ("match", "strict", ["lines"]), + ("match", "exact", ["lines"]), + ("replace", "block", ["lines"]), + ("replace", "config", ["replace_src"]), + ("diff_against", "intended", ["intended_config"]), + ] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False, "warnings": warnings} + + config = None + + diff_ignore_lines = module.params["diff_ignore_lines"] + path = module.params["parents"] + connection = get_connection(module) + contents = None + flags = ["all"] if module.params["defaults"] else [] + replace_src = module.params["replace_src"] + if replace_src: + if module.params["replace"] != "config": + module.fail_json(msg="replace: config is required with replace_src") + + if module.params["backup"] or (module._diff and module.params["diff_against"] == "running"): + contents = get_config(module, flags=flags) + config = NetworkConfig(indent=2, contents=contents) + if module.params["backup"]: + result["__backup__"] = contents + + if any((module.params["src"], module.params["lines"], replace_src)): + match = module.params["match"] + replace = module.params["replace"] + + commit = not module.check_mode + candidate = get_candidate(module) + running = get_running_config(module, contents, flags=flags) + if replace_src: + commands = candidate.split("\n") + result["commands"] = result["updates"] = commands + if commit: + load_config(module, commands, replace=replace_src) + + result["changed"] = True + else: + try: + response = connection.get_diff( + candidate=candidate, + running=running, + diff_match=match, + diff_ignore_lines=diff_ignore_lines, + path=path, + diff_replace=replace, + ) + except ConnectionError as exc: + module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) + + config_diff = response["config_diff"] + if config_diff: + commands = config_diff.split("\n") + + if module.params["before"]: + commands[:0] = module.params["before"] + + if module.params["after"]: + commands.extend(module.params["after"]) + + result["commands"] = commands + result["updates"] = commands + + if commit: + load_config(module, commands, replace=replace_src) + + result["changed"] = True + + running_config = module.params["running_config"] + startup_config = None + + if module.params["save_when"] == "always": + save_config(module, result) + elif module.params["save_when"] == "modified": + output = execute_show_commands(module, ["show running-config", "show startup-config"]) + + running_config = NetworkConfig(indent=2, contents=output[0], ignore_lines=diff_ignore_lines) + startup_config = NetworkConfig(indent=2, contents=output[1], ignore_lines=diff_ignore_lines) + + if running_config.sha1 != startup_config.sha1: + save_config(module, result) + elif module.params["save_when"] == "changed" and result["changed"]: + save_config(module, result) + + if module._diff: + if not running_config: + output = execute_show_commands(module, "show running-config") + contents = output[0] + else: + contents = running_config + + # recreate the object in order to process diff_ignore_lines + running_config = NetworkConfig(indent=2, contents=contents, ignore_lines=diff_ignore_lines) + + if module.params["diff_against"] == "running": + if module.check_mode: + module.warn("unable to perform diff against running-config due to check mode") + contents = None + else: + contents = config.config_text + + elif module.params["diff_against"] == "startup": + if not startup_config: + output = execute_show_commands(module, "show startup-config") + contents = output[0] + else: + contents = startup_config.config_text + + elif module.params["diff_against"] == "intended": + contents = module.params["intended_config"] + + if contents is not None: + base_config = NetworkConfig(indent=2, contents=contents, ignore_lines=diff_ignore_lines) + + if running_config.sha1 != base_config.sha1: + before = "" + after = "" + if module.params["diff_against"] == "intended": + before = running_config + after = base_config + elif module.params["diff_against"] in ("startup", "running"): + before = base_config + after = running_config + + result.update( + { + "changed": True, + "diff": {"before": str(before), "after": str(after)}, + }, + ) + + if result.get("changed") and any((module.params["src"], module.params["lines"])): + msg = ( + "To ensure idempotency and correct diff the input configuration lines should be" + " similar to how they appear if present in" + " the running configuration on device" + ) + if module.params["src"]: + msg += " including the indentation" + if "warnings" in result: + result["warnings"].append(msg) + else: + result["warnings"] = msg + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_devicealias.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_devicealias.py new file mode 100644 index 00000000..71d4ebb6 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_devicealias.py @@ -0,0 +1,550 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_devicealias +short_description: Configuration of device alias for Cisco NXOS MDS Switches. +description: +- Configuration of device alias for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + distribute: + description: + - Enable/Disable device-alias distribution + type: bool + mode: + description: + - Mode of devices-alias, basic or enhanced + choices: + - basic + - enhanced + type: str + da: + description: + - List of device-alias to be added or removed + type: list + elements: dict + suboptions: + name: + description: + - Name of the device-alias to be added or removed + required: true + type: str + pwwn: + description: + - pwwn to which the name needs to be associated with + type: str + remove: + description: + - Removes the device-alias if set to True + type: bool + default: false + rename: + description: + - List of device-alias to be renamed + type: list + elements: dict + suboptions: + old_name: + description: + - Old name of the device-alias that needs to be renamed + required: true + type: str + new_name: + description: + - New name of the device-alias + required: true + type: str +""" + +EXAMPLES = """ +- name: Test that device alias module works + cisco.nxos.nxos_devicealias: + da: + - name: test1_add + pwwn: 56:2:22:11:22:88:11:67 + - name: test2_add + pwwn: 65:22:22:11:22:22:11:d + - name: dev1 + remove: true + - name: dev2 + remove: true + distribute: true + mode: enhanced + rename: + - new_name: bcd + old_name: abc + - new_name: bcd1 + old_name: abc1 + + +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - device-alias database + - device-alias name somename pwwn 10:00:00:00:89:a1:01:03 + - device-alias name somename1 pwwn 10:00:00:00:89:a1:02:03 + - device-alias commit + - no terminal dont-ask +""" + +import string + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + +VALID_DA_CHARS = ("-", "_", "$", "^") + + +class showDeviceAliasStatus(object): + """docstring for showDeviceAliasStatus""" + + def __init__(self, module): + self.module = module + self.distribute = "" + self.mode = "" + self.locked = False + self.update() + + def execute_show_cmd(self, cmd): + output = execute_show_command(cmd, self.module)[0] + return output + + def update(self): + command = "show device-alias status" + output = self.execute_show_cmd(command).split("\n") + for o in output: + if "Fabric Distribution" in o: + self.distribute = o.split(":")[1].strip().lower() + if "Mode" in o: + self.mode = o.split("Mode:")[1].strip().lower() + if "Locked" in o: + self.locked = True + + def isLocked(self): + return self.locked + + def getDistribute(self): + return self.distribute + + def getMode(self): + return self.mode + + +class showDeviceAliasDatabase(object): + """docstring for showDeviceAliasDatabase""" + + def __init__(self, module): + self.module = module + self.da_dict = {} + self.update() + + def execute_show_cmd(self, cmd): + output = execute_show_command(cmd, self.module)[0] + return output + + def update(self): + command = "show device-alias database" + # output = execute_show_command(command, self.module)[0].split("\n") + output = self.execute_show_cmd(command) + self.da_list = output.split("\n") + for eachline in self.da_list: + if "device-alias" in eachline: + sv = eachline.strip().split() + self.da_dict[sv[2]] = sv[4] + + def isNameInDaDatabase(self, name): + return name in self.da_dict.keys() + + def isPwwnInDaDatabase(self, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + return newpwwn in self.da_dict.values() + + def isNamePwwnPresentInDatabase(self, name, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + if name in self.da_dict.keys(): + if newpwwn == self.da_dict[name]: + return True + return False + + def getPwwnByName(self, name): + if name in self.da_dict.keys(): + return self.da_dict[name] + else: + return None + + def getNameByPwwn(self, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + for n, p in self.da_dict.items(): + if p == newpwwn: + return n + return None + + +def isPwwnValid(pwwn): + pwwnsplit = pwwn.split(":") + if len(pwwnsplit) != 8: + return False + for eachpwwnsplit in pwwnsplit: + if len(eachpwwnsplit) > 2 or len(eachpwwnsplit) < 1: + return False + if not all(c in string.hexdigits for c in eachpwwnsplit): + return False + return True + + +def isNameValid(name): + if not name[0].isalpha(): + # Illegal first character. Name must start with a letter + return False + if len(name) > 64: + return False + for character in name: + if not character.isalnum() and character not in VALID_DA_CHARS: + return False + return True + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + out = run_commands(module, commands) + return out + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def main(): + element_spec = dict( + name=dict(required=True, type="str"), + pwwn=dict(type="str"), + remove=dict(type="bool", default=False), + ) + + element_spec_rename = dict( + old_name=dict(required=True, type="str"), + new_name=dict(required=True, type="str"), + ) + + argument_spec = dict( + distribute=dict(type="bool"), + mode=dict(type="str", choices=["enhanced", "basic"]), + da=dict(type="list", elements="dict", options=element_spec), + rename=dict(type="list", elements="dict", options=element_spec_rename), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + messages = list() + commands_to_execute = list() + result = {"changed": False} + + distribute = module.params["distribute"] + mode = module.params["mode"] + da = module.params["da"] + rename = module.params["rename"] + + # Step 0.0: Validate syntax of name and pwwn + # Also validate syntax of rename arguments + if da is not None: + for eachdict in da: + name = eachdict["name"] + pwwn = eachdict["pwwn"] + remove = eachdict["remove"] + if pwwn is not None: + pwwn = pwwn.lower() + if not remove: + if pwwn is None: + module.fail_json( + msg="This device alias name " + + str(name) + + " which needs to be added, does not have pwwn specified. Please specify a valid pwwn", + ) + if not isNameValid(name): + module.fail_json( + msg="This pwwn name is invalid : " + + str(name) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + if not isPwwnValid(pwwn): + module.fail_json( + msg="This pwwn is invalid : " + + str(pwwn) + + ". Please check that its a valid pwwn", + ) + if rename is not None: + for eachdict in rename: + oldname = eachdict["old_name"] + newname = eachdict["new_name"] + if not isNameValid(oldname): + module.fail_json( + msg="This pwwn name is invalid : " + + str(oldname) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + if not isNameValid(newname): + module.fail_json( + msg="This pwwn name is invalid : " + + str(newname) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + + # Step 0.1: Check DA status + shDAStausObj = showDeviceAliasStatus(module) + d = shDAStausObj.getDistribute() + m = shDAStausObj.getMode() + if shDAStausObj.isLocked(): + module.fail_json(msg="device-alias has acquired lock on the switch. Hence cannot procced.") + + # Step 1: Process distribute + commands = [] + if distribute is not None: + if distribute: + # playbook has distribute as True(enabled) + if d == "disabled": + # but switch distribute is disabled(false), so set it to + # true(enabled) + commands.append("device-alias distribute") + messages.append("device-alias distribute changed from disabled to enabled") + else: + messages.append( + "device-alias distribute remains unchanged. current distribution mode is enabled", + ) + else: + # playbook has distribute as False(disabled) + if d == "enabled": + # but switch distribute is enabled(true), so set it to + # false(disabled) + commands.append("no device-alias distribute") + messages.append("device-alias distribute changed from enabled to disabled") + else: + messages.append( + "device-alias distribute remains unchanged. current distribution mode is disabled", + ) + + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the da_add/da_remove stage + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step 2: Process mode + commands = [] + if mode is not None: + if mode == "basic": + # playbook has mode as basic + if m == "enhanced": + # but switch mode is enhanced, so set it to basic + commands.append("no device-alias mode enhanced") + messages.append("device-alias mode changed from enhanced to basic") + else: + messages.append("device-alias mode remains unchanged. current mode is basic") + + else: + # playbook has mode as enhanced + if m == "basic": + # but switch mode is basic, so set it to enhanced + commands.append("device-alias mode enhanced") + messages.append("device-alias mode changed from basic to enhanced") + else: + messages.append("device-alias mode remains unchanged. current mode is enhanced") + + if commands: + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step 3: Process da + commands = [] + shDADatabaseObj = showDeviceAliasDatabase(module) + if da is not None: + da_remove_list = [] + da_add_list = [] + for eachdict in da: + name = eachdict["name"] + pwwn = eachdict["pwwn"] + remove = eachdict["remove"] + if pwwn is not None: + pwwn = pwwn.lower() + if remove: + if shDADatabaseObj.isNameInDaDatabase(name): + commands.append("no device-alias name " + name) + da_remove_list.append(name) + else: + messages.append( + name + + " - This device alias name is not in switch device-alias database, hence cannot be removed.", + ) + else: + if shDADatabaseObj.isNamePwwnPresentInDatabase(name, pwwn): + messages.append( + name + + " : " + + pwwn + + " - This device alias name,pwwn is already in switch device-alias database, hence nothing to configure", + ) + else: + if shDADatabaseObj.isNameInDaDatabase(name): + module.fail_json( + msg=name + + " - This device alias name is already present in switch device-alias database but assigned to another pwwn (" + + shDADatabaseObj.getPwwnByName(name) + + ") hence cannot be added", + ) + + elif shDADatabaseObj.isPwwnInDaDatabase(pwwn): + module.fail_json( + msg=pwwn + + " - This device alias pwwn is already present in switch device-alias database but assigned to another name (" + + shDADatabaseObj.getNameByPwwn(pwwn) + + ") hence cannot be added", + ) + + else: + commands.append("device-alias name " + name + " pwwn " + pwwn) + da_add_list.append(name) + + if len(da_add_list) != 0 or len(da_remove_list) != 0: + commands = ["device-alias database"] + commands + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + if len(da_remove_list) != 0: + messages.append( + "the required device-alias were removed. " + ",".join(da_remove_list), + ) + if len(da_add_list) != 0: + messages.append( + "the required device-alias were added. " + ",".join(da_add_list), + ) + + # Step 5: Process rename + commands = [] + if rename is not None: + for eachdict in rename: + oldname = eachdict["old_name"] + newname = eachdict["new_name"] + if shDADatabaseObj.isNameInDaDatabase(newname): + module.fail_json( + changed=False, + commands=cmds, + msg=newname + + " - this name is already present in the device-alias database, hence we cannot rename " + + oldname + + " with this one", + ) + if shDADatabaseObj.isNameInDaDatabase(oldname): + commands.append("device-alias rename " + oldname + " " + newname) + else: + module.fail_json( + changed=False, + commands=cmds, + msg=oldname + + " - this name is not present in the device-alias database, hence we cannot rename.", + ) + + if len(commands) != 0: + commands = ["device-alias database"] + commands + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step END: check for 'check' mode + if module.check_mode: + module.exit_json( + changed=False, + commands=commands_to_execute, + msg="Check Mode: No cmds issued to the hosts", + ) + + result["messages"] = messages + result["commands"] = commands_to_execute + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_global.py new file mode 100644 index 00000000..83ef7839 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_global.py @@ -0,0 +1,103 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_evpn_global +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Handles the EVPN control plane for VXLAN. +description: +- Handles the EVPN control plane for VXLAN. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- This module is not supported on Nexus 3000 series of switches. +- Unsupported for Cisco MDS +options: + nv_overlay_evpn: + description: + - EVPN control plane. + required: true + type: bool +""" + +EXAMPLES = """ +- cisco.nxos.nxos_evpn_global: + nv_overlay_evpn: true +""" + +RETURN = """ +commands: + description: The set of commands to be sent to the remote device + returned: always + type: list + sample: ['nv overlay evpn'] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_config, + load_config, +) + + +def main(): + argument_spec = dict(nv_overlay_evpn=dict(required=True, type="bool")) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + result = {"changed": False} + + warnings = list() + if warnings: + result["warnings"] = warnings + + config = get_config(module) + commands = list() + + info = get_capabilities(module).get("device_info", {}) + os_platform = info.get("network_os_platform", "") + + if "3K" in os_platform: + module.fail_json(msg="This module is not supported on Nexus 3000 series") + + if module.params["nv_overlay_evpn"] is True: + if "nv overlay evpn" not in config: + commands.append("nv overlay evpn") + elif "nv overlay evpn" in config: + commands.append("no nv overlay evpn") + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + result["commands"] = commands + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_vni.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_vni.py new file mode 100644 index 00000000..d4490bb7 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_evpn_vni.py @@ -0,0 +1,299 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_evpn_vni +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI). +description: +- Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network Identifier (VNI) + configurations of a Nexus device. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- default, where supported, restores params default value. +- RD override is not permitted. You should set it to the default values first and + then reconfigure it. +- C(route_target_both), C(route_target_import) and C(route_target_export valid) values + are a list of extended communities, (i.e. ['1.2.3.4:5', '33:55']) or the keywords + 'auto' or 'default'. +- The C(route_target_both) property is discouraged due to the inconsistent behavior + of the property across Nexus platforms and image versions. For this reason it is + recommended to use explicit C(route_target_export) and C(route_target_import) properties + instead of C(route_target_both). +- RD valid values are a string in one of the route-distinguisher formats, the keyword + 'auto', or the keyword 'default'. +options: + vni: + description: + - The EVPN VXLAN Network Identifier. + required: true + type: str + route_distinguisher: + description: + - The VPN Route Distinguisher (RD). The RD is combined with the IPv4 or IPv6 prefix + learned by the PE router to create a globally unique address. + type: str + route_target_both: + description: + - Enables/Disables route-target settings for both import and export target communities + using a single property. + type: list + elements: str + route_target_import: + description: + - Sets the route-target 'import' extended communities. + type: list + elements: str + route_target_export: + description: + - Sets the route-target 'export' extended communities. + type: list + elements: str + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: vni configuration + cisco.nxos.nxos_evpn_vni: + vni: 6000 + route_distinguisher: 60:10 + route_target_import: + - 5000:10 + - 4100:100 + route_target_export: auto + route_target_both: default +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["evpn", "vni 6000 l2", "route-target import 5001:10"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +PARAM_TO_COMMAND_KEYMAP = { + "vni": "vni", + "route_distinguisher": "rd", + "route_target_both": "route-target both", + "route_target_import": "route-target import", + "route_target_export": "route-target export", +} + + +def get_value(arg, config, module): + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + command_re = re.compile(r"(?:{0}\s)(?P<value>.*)$".format(command), re.M) + value = "" + if command in config: + value = command_re.search(config).group("value") + return value + + +def get_route_target_value(arg, config, module): + splitted_config = config.splitlines() + value_list = [] + command = PARAM_TO_COMMAND_KEYMAP.get(arg) + command_re = re.compile(r"(?:{0}\s)(?P<value>.*)$".format(command), re.M) + + for line in splitted_config: + value = "" + if command in line.strip(): + value = command_re.search(line).group("value") + value_list.append(value) + return value_list + + +def get_existing(module, args): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + parents = ["evpn", "vni {0} l2".format(module.params["vni"])] + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg != "vni": + if arg == "route_distinguisher": + existing[arg] = get_value(arg, config, module) + else: + existing[arg] = get_route_target_value(arg, config, module) + + existing_fix = dict((k, v) for k, v in existing.items() if v) + if not existing_fix: + existing = existing_fix + + existing["vni"] = module.params["vni"] + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key in table: + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = table.get(key) + return new_dict + + +def fix_proposed(proposed_commands): + new_proposed = {} + for key, value in proposed_commands.items(): + if key == "route-target both": + new_proposed["route-target export"] = value + new_proposed["route-target import"] = value + else: + new_proposed[key] = value + return new_proposed + + +def state_present(module, existing, proposed): + commands = list() + parents = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + if proposed_commands.get("route-target both"): + proposed_commands = fix_proposed(proposed_commands) + + for key, value in proposed_commands.items(): + if key.startswith("route-target"): + if value == ["default"]: + existing_value = existing_commands.get(key) + + if existing_value: + for target in existing_value: + commands.append("no {0} {1}".format(key, target)) + elif not isinstance(value, list): + value = [value] + + for target in value: + if target == "default": + continue + if existing: + if target not in existing.get(key.replace("-", "_").replace(" ", "_")): + commands.append("{0} {1}".format(key, target)) + else: + commands.append("{0} {1}".format(key, target)) + + if existing.get(key.replace("-", "_").replace(" ", "_")): + for exi in existing.get(key.replace("-", "_").replace(" ", "_")): + if exi not in value: + commands.append("no {0} {1}".format(key, exi)) + + elif value == "default": + existing_value = existing_commands.get(key) + if existing_value: + commands.append("no {0} {1}".format(key, existing_value)) + else: + command = "{0} {1}".format(key, value) + commands.append(command) + + if commands: + parents = ["evpn", "vni {0} l2".format(module.params["vni"])] + + return commands, parents + + +def state_absent(module, existing, proposed): + commands = ["no vni {0} l2".format(module.params["vni"])] + parents = ["evpn"] + return commands, parents + + +def main(): + argument_spec = dict( + vni=dict(required=True, type="str"), + route_distinguisher=dict(required=False, type="str"), + route_target_both=dict(required=False, type="list", elements="str"), + route_target_import=dict(required=False, type="list", elements="str"), + route_target_export=dict(required=False, type="list", elements="str"), + state=dict(choices=["present", "absent"], default="present", required=False), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = dict(changed=False, warnings=warnings) + + state = module.params["state"] + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing = get_existing(module, args) + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + commands = [] + parents = [] + + proposed = {} + for key, value in proposed_args.items(): + if key != "vni": + if value == "true": + value = True + elif value == "false": + value = False + if existing.get(key) != value: + proposed[key] = value + + if state == "present": + commands, parents = state_present(module, existing, proposed) + elif state == "absent" and existing: + commands, parents = state_absent(module, existing, proposed) + + if commands: + candidate = CustomNetworkConfig(indent=3) + candidate.add(commands, parents=parents) + candidate = candidate.items_text() + if not module.check_mode: + load_config(module, candidate) + results["changed"] = True + results["commands"] = candidate + else: + results["commands"] = [] + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_facts.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_facts.py new file mode 100644 index 00000000..24e0dad2 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_facts.py @@ -0,0 +1,268 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_facts +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Gets facts about NX-OS switches +description: +- Collects facts from Cisco Nexus devices running the NX-OS operating system. Fact + collection is supported over both C(network_cli) and C(httpapi). This module prepends + all of the base network fact keys with C(ansible_net_<fact>). The facts module + will always collect a base set of facts from the device and can enable or disable + collection of additional facts. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + gather_subset: + description: + - When supplied, this argument will gather operational facts only for the given subset. Possible + values for this argument include C(all), C(hardware), C(config), C(legacy), C(interfaces), and C(min). Can + specify a list of values to include a larger subset. Values can also be used + with an initial C(!) to specify that a specific subset should not be collected. + required: false + default: 'min' + type: list + elements: str + gather_network_resources: + description: + - When supplied, this argument will gather configuration facts only for the given subset. + Can specify a list of values to include a larger subset. Values can + also be used with an initial C(!) to specify that a specific subset should + not be collected. + - Valid subsets are C(all), C(bfd_interfaces), C(lag_interfaces), + C(telemetry), C(vlans), C(lacp), C(lacp_interfaces), C(interfaces), C(l3_interfaces), + C(l2_interfaces), C(lldp_global), C(acls), C(acl_interfaces), C(ospfv2), C(ospfv3), C(ospf_interfaces), + C(bgp_global), C(bgp_address_family), C(route_maps), C(prefix_lists), C(logging_global), C(ntp_global), + C(snmp_server), C(hostname). + required: false + type: list + elements: str + available_network_resources: + description: When set to C(true) a list of network resources for which resource modules are available will be provided. + type: bool + default: false +""" + +EXAMPLES = """ +- name: Gather all legacy facts + cisco.nxos.nxos_facts: + gather_subset: all +- name: Gather only the config and default facts + cisco.nxos.nxos_facts: + gather_subset: + - config +- name: Do not gather hardware facts + cisco.nxos.nxos_facts: + gather_subset: + - '!hardware' +- name: Gather legacy and resource facts + cisco.nxos.nxos_facts: + gather_subset: all + gather_network_resources: all +- name: Gather only the interfaces resource facts and no legacy facts + cisco.nxos.nxos_facts: + gather_subset: + - '!all' + - '!min' + gather_network_resources: + - interfaces +- name: Gather interfaces resource and minimal legacy facts + cisco.nxos.nxos_facts: + gather_subset: min + gather_network_resources: interfaces +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list +ansible_net_gather_network_resources: + description: The list of fact for network resource subsets collected from the device + returned: when the resource is configured + type: list +# default +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_serialnum: + description: The serial number of the remote device + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str +ansible_net_image: + description: The image file the device is running + returned: always + type: str +ansible_net_api: + description: The name of the transport + returned: always + type: str +ansible_net_license_hostid: + description: The License host id of the device + returned: always + type: str +ansible_net_python_version: + description: The Python version Ansible controller is using + returned: always + type: str +# hardware +ansible_net_filesystems: + description: All file system names available on the device + returned: when hardware is configured + type: list +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str +# interfaces +ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_neighbors: + description: + - The list of LLDP and CDP neighbors from the device. If both, + CDP and LLDP neighbor data is present on one port, CDP is preferred. + returned: when interfaces is configured + type: dict +# legacy (pre Ansible 2.2) +fan_info: + description: A hash of facts about fans in the remote device + returned: when legacy is configured + type: dict +hostname: + description: The configured hostname of the remote device + returned: when legacy is configured + type: dict +interfaces_list: + description: The list of interface names on the remote device + returned: when legacy is configured + type: dict +kickstart: + description: The software version used to boot the system + returned: when legacy is configured + type: str +module: + description: A hash of facts about the modules in a remote device + returned: when legacy is configured + type: dict +platform: + description: The hardware platform reported by the remote device + returned: when legacy is configured + type: str +power_supply_info: + description: A hash of facts about the power supplies in the remote device + returned: when legacy is configured + type: str +vlan_list: + description: The list of VLAN IDs configured on the remote device + returned: when legacy is configured + type: list +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.facts.facts import ( + FactsArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.facts.facts import Facts + + +def get_chassis_type(connection): + """Return facts resource subsets based on + chassis model. + """ + target_type = "nexus" + + device_info = connection.get_device_info() + model = device_info.get("network_os_model", "") + platform = device_info.get("network_os_platform", "") + + if platform.startswith("DS-") and "MDS" in model: + target_type = "mds" + + return target_type + + +def main(): + """ + Main entry point for module execution + + :returns: ansible_facts + """ + argument_spec = FactsArgs.argument_spec + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + connection = Connection(module._socket_path) + facts = Facts(module, chassis_type=get_chassis_type(connection)) + + warnings = [] + + ansible_facts = {} + if module.params.get("available_network_resources"): + ansible_facts["available_network_resources"] = sorted(facts.get_resource_subsets().keys()) + + result = facts.get_facts() + additional_facts, additional_warnings = result + ansible_facts.update(additional_facts) + warnings.extend(additional_warnings) + + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_feature.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_feature.py new file mode 100644 index 00000000..c9229c4d --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_feature.py @@ -0,0 +1,308 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_feature +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manage features in NX-OS switches. +notes: +- Tested against Cisco MDS NX-OS 9.2(2) +description: +- Offers ability to enable and disable features in NX-OS. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +- Suhas Bharadwaj (@srbharadwaj) +options: + feature: + description: + - Name of feature. + required: true + type: str + state: + description: + - Desired state of the feature. + required: false + default: enabled + choices: + - enabled + - disabled + type: str +""" + +EXAMPLES = """ +- name: Ensure lacp is enabled + cisco.nxos.nxos_feature: + feature: lacp + state: enabled + +- name: Ensure ospf is disabled + cisco.nxos.nxos_feature: + feature: ospf + state: disabled + +- name: Ensure vpc is enabled + cisco.nxos.nxos_feature: + feature: vpc + state: enabled +""" + +RETURN = """ +commands: + description: The set of commands to be sent to the remote device + returned: always + type: list + sample: ['nv overlay evpn'] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import ConnectionError + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_config, + load_config, + run_commands, +) + + +def get_available_features(feature, module): + available_features = {} + feature_regex = r"(?P<feature>\S+)\s+\d+\s+(?P<state>.*)" + command = {"command": "show feature", "output": "text"} + + try: + body = run_commands(module, [command])[0] + split_body = body.splitlines() + except (KeyError, IndexError): + return {} + + for line in split_body: + try: + match_feature = re.match(feature_regex, line, re.DOTALL) + feature_group = match_feature.groupdict() + feature = feature_group["feature"] + state = feature_group["state"] + except AttributeError: + feature = "" + state = "" + + if feature and state: + if "enabled" in state: + state = "enabled" + + if feature not in available_features: + available_features[feature] = state + else: + if available_features[feature] == "disabled" and state == "enabled": + available_features[feature] = state + + # certain configurable features do not + # show up in the output of "show feature" + # but appear in running-config when set + run_cfg = get_config(module, flags=["| include ^feature"]) + for item in re.findall(r"feature\s(.*)", run_cfg): + if item not in available_features: + available_features[item] = "enabled" + + if "fabric forwarding" not in available_features: + available_features["fabric forwarding"] = "disabled" + + return available_features + + +def get_commands(proposed, existing, state, module): + feature = validate_feature(module, mode="config") + commands = [] + feature_check = proposed == existing + if not feature_check: + if state == "enabled": + command = "feature {0}".format(feature) + commands.append(command) + elif state == "disabled": + command = "no feature {0}".format(feature) + commands.append(command) + return commands + + +def get_mds_mapping_features(): + feature_to_be_mapped = { + "show": { + "fcrxbbcredit": "extended_credit", + "port-track": "port_track", + "scp-server": "scpServer", + "sftp-server": "sftpServer", + "ssh": "sshServer", + "tacacs+": "tacacs", + "telnet": "telnetServer", + }, + "config": { + "extended_credit": "fcrxbbcredit", + "port_track": "port-track", + "scpServer": "scp-server", + "sftpServer": "sftp-server", + "sshServer": "ssh", + "tacacs": "tacacs+", + "telnetServer": "telnet", + }, + } + return feature_to_be_mapped + + +def validate_feature(module, mode="show"): + """Some features may need to be mapped due to inconsistency + between how they appear from "show feature" output and + how they are configured""" + + feature = module.params["feature"] + + try: + info = get_capabilities(module) + device_info = info.get("device_info", {}) + os_version = device_info.get("network_os_version", "") + os_platform = device_info.get("network_os_platform", "") + except ConnectionError: + os_version = "" + os_platform = "" + + if "8.1" in os_version: + feature_to_be_mapped = { + "show": { + "nv overlay": "nve", + "vn-segment-vlan-based": "vnseg_vlan", + "hsrp": "hsrp_engine", + "fabric multicast": "fabric_mcast", + "scp-server": "scpServer", + "sftp-server": "sftpServer", + "sla responder": "sla_responder", + "sla sender": "sla_sender", + "ssh": "sshServer", + "tacacs+": "tacacs", + "telnet": "telnetServer", + "ethernet-link-oam": "elo", + }, + "config": { + "nve": "nv overlay", + "vnseg_vlan": "vn-segment-vlan-based", + "hsrp_engine": "hsrp", + "fabric_mcast": "fabric multicast", + "scpServer": "scp-server", + "sftpServer": "sftp-server", + "sla_sender": "sla sender", + "sla_responder": "sla responder", + "sshServer": "ssh", + "tacacs": "tacacs+", + "telnetServer": "telnet", + "elo": "ethernet-link-oam", + }, + } + else: + feature_to_be_mapped = { + "show": { + "nv overlay": "nve", + "vn-segment-vlan-based": "vnseg_vlan", + "hsrp": "hsrp_engine", + "fabric multicast": "fabric_mcast", + "scp-server": "scpServer", + "sftp-server": "sftpServer", + "sla responder": "sla_responder", + "sla sender": "sla_sender", + "ssh": "sshServer", + "tacacs+": "tacacs", + "telnet": "telnetServer", + "ethernet-link-oam": "elo", + "port-security": "eth_port_sec", + }, + "config": { + "nve": "nv overlay", + "vnseg_vlan": "vn-segment-vlan-based", + "hsrp_engine": "hsrp", + "fabric_mcast": "fabric multicast", + "scpServer": "scp-server", + "sftpServer": "sftp-server", + "sla_sender": "sla sender", + "sla_responder": "sla responder", + "sshServer": "ssh", + "tacacs": "tacacs+", + "telnetServer": "telnet", + "elo": "ethernet-link-oam", + "eth_port_sec": "port-security", + }, + } + + if os_platform.startswith("DS-"): + feature_to_be_mapped = get_mds_mapping_features() + + if feature in feature_to_be_mapped[mode]: + feature = feature_to_be_mapped[mode][feature] + + return feature + + +def main(): + argument_spec = dict( + feature=dict(type="str", required=True), + state=dict(choices=["enabled", "disabled"], default="enabled"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = dict(changed=False, warnings=warnings) + + feature = validate_feature(module) + state = module.params["state"].lower() + + available_features = get_available_features(feature, module) + if feature not in available_features: + module.fail_json( + msg="Invalid feature name.", + features_currently_supported=available_features, + invalid_feature=feature, + ) + else: + existstate = available_features[feature] + + existing = dict(state=existstate) + proposed = dict(state=state) + results["changed"] = False + + cmds = get_commands(proposed, existing, state, module) + + if cmds: + # On N35 A8 images, some features return a yes/no prompt + # on enablement or disablement. Bypass using terminal dont-ask + cmds.insert(0, "terminal dont-ask") + if not module.check_mode: + load_config(module, cmds) + results["changed"] = True + + results["commands"] = cmds + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_file_copy.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_file_copy.py new file mode 100644 index 00000000..4847a446 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_file_copy.py @@ -0,0 +1,497 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_file_copy +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Copy a file to a remote NXOS device. +description: +- This module supports two different workflows for copying a file to flash (or bootflash) + on NXOS devices. Files can either be (1) pushed from the Ansible controller to + the device or (2) pulled from a remote SCP file server to the device. File copies + are initiated from the NXOS device to the remote SCP server. This module only supports + the use of connection C(network_cli) or C(Cli) transport with connection C(local). +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +- Rewritten as a plugin by (@mikewiebe) +notes: +- Tested against NXOS 7.0(3)I2(5), 7.0(3)I4(6), 7.0(3)I5(3), 7.0(3)I6(1), 7.0(3)I7(3), + 6.0(2)A8(8), 7.0(3)F3(4), 7.3(0)D1(1), 8.3(0), 9.2, 9.3 +- Limited Support for Cisco MDS +- When pushing files (file_pull is False) to the NXOS device, feature scp-server must + be enabled. +- When pulling files (file_pull is True) to the NXOS device, feature scp-server is + not required. +- When pulling files (file_pull is True) to the NXOS device, no transfer will take + place if the file is already present. +- Check mode will tell you if the file would be copied. +requirements: +- paramiko or libssh (required when file_pull is False) +- scp (required when file_pull is False) +options: + local_file: + description: + - When (file_pull is False) this is the path to the local file on the Ansible + controller. The local directory must exist. + - When (file_pull is True) this is the target file name on the NXOS device. + type: path + remote_file: + description: + - When (file_pull is False) this is the remote file path on the NXOS device. If + omitted, the name of the local file will be used. The remote directory must + exist. + - When (file_pull is True) this is the full path to the file on the remote SCP + server to be copied to the NXOS device. + type: path + file_system: + description: + - The remote file system on the nxos device. If omitted, devices that support + a I(file_system) parameter will use their default values. + default: 'bootflash:' + type: str + connect_ssh_port: + description: + - B(Deprecated) + - This option has been deprecated and will be removed in a release after 2024-06-01. + - To maintain backwards compatibility, this option will continue to override the value of I(ansible_port) until removed. + - HORIZONTALLINE + - SSH server port used for file transfer. + - Only used when I(file_pull) is C(True). + default: 22 + type: int + file_pull: + description: + - When (False) file is copied from the Ansible controller to the NXOS device. + - When (True) file is copied from a remote SCP server to the NXOS device. In this + mode, the file copy is initiated from the NXOS device. + - If the file is already present on the device it will be overwritten and therefore + the operation is NOT idempotent. + type: bool + default: false + file_pull_protocol: + description: + - When file_pull is True, this can be used to define the transfer protocol for + copying file from remote to the NXOS device. + - When (file_pull is False), this is not used. + default: 'scp' + choices: + - scp + - sftp + - ftp + - http + - https + - tftp + type: str + file_pull_compact: + description: + - When file_pull is True, this is used to compact nxos image files. This option + can only be used with nxos image files. + - When (file_pull is False), this is not used. + type: bool + default: false + file_pull_kstack: + description: + - When file_pull is True, this can be used to speed up file copies when the nxos + running image supports the use-kstack option. + - When (file_pull is False), this is not used. + type: bool + default: false + local_file_directory: + description: + - When (file_pull is True) file is copied from a remote SCP server to the NXOS + device, and written to this directory on the NXOS device. If the directory does + not exist, it will be created under the file_system. This is an optional parameter. + - When (file_pull is False), this is not used. + type: path + file_pull_timeout: + description: + - B(Deprecated) + - This option has been deprecated and will be removed in a release after 2024-06-01. + - To maintain backwards compatibility, this option will continue to override the value of I(ansible_command_timeout) until removed. + - HORIZONTALLINE + - Use this parameter to set timeout in seconds, when transferring large files + or when the network is slow. + - When (file_pull is False), this is not used. + default: 300 + type: int + remote_scp_server: + description: + - The remote scp server address when file_pull is True. This is required if file_pull + is True. + - When (file_pull is False), this is not used. + type: str + remote_scp_server_user: + description: + - The remote scp server username when file_pull is True. This is required if file_pull + is True. + - When (file_pull is False), this is not used. + type: str + remote_scp_server_password: + description: + - The remote scp server password when file_pull is True. This is required if file_pull + is True. + - When (file_pull is False), this is not used. + type: str + vrf: + description: + - The VRF used to pull the file. Useful when no vrf management is defined. + - This option is not applicable for MDS switches. + default: management + type: str +""" + +EXAMPLES = """ +# File copy from ansible controller to nxos device +- name: copy from server to device + cisco.nxos.nxos_file_copy: + local_file: ./test_file.txt + remote_file: test_file.txt + +# Initiate file copy from the nxos device to transfer file from an SCP server back to the nxos device +- name: initiate file copy from device + cisco.nxos.nxos_file_copy: + file_pull: true + local_file: xyz + local_file_directory: dir1/dir2/dir3 + remote_file: /mydir/abc + remote_scp_server: 192.168.0.1 + remote_scp_server_user: myUser + remote_scp_server_password: myPassword + vrf: management + +# Initiate file copy from the nxos device to transfer file from a ftp server back to the nxos device. +# remote_scp_server_user and remote_scp_server_password are used to login to the FTP server. +- name: initiate file copy from device + cisco.nxos.nxos_file_copy: + file_pull: true + file_pull_protocol: ftp + local_file: xyz + remote_file: /mydir/abc + remote_scp_server: 192.168.0.1 + remote_scp_server_user: myUser + remote_scp_server_password: myPassword + vrf: management +""" + +RETURN = """ +transfer_status: + description: Whether a file was transferred to the nxos device. + returned: success + type: str + sample: 'Sent' +local_file: + description: The path of the local file. + returned: success + type: str + sample: '/path/to/local/file' +remote_file: + description: The path of the remote file. + returned: success + type: str + sample: '/path/to/remote/file' +remote_scp_server: + description: The name of the scp server when file_pull is True. + returned: success + type: str + sample: 'fileserver.example.com' +changed: + description: Indicates whether or not the file was copied. + returned: success + type: bool + sample: true +""" + +import hashlib +import os +import re + +from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network import ( + get_resource_connection, +) + + +class FileCopy: + def __init__(self, module): + self._module = module + self._connection = get_resource_connection(self._module) + device_info = self._connection.get_device_info() + self._model = device_info.get("network_os_model", "") + self._platform = device_info.get("network_os_platform", "") + + +class FilePush(FileCopy): + def __init__(self, module): + super(FilePush, self).__init__(module) + self.result = {} + + def md5sum_check(self, dst, file_system): + command = "show file {0}{1} md5sum".format(file_system, dst) + remote_filehash = self._connection.run_commands(command)[0] + remote_filehash = to_bytes(remote_filehash, errors="surrogate_or_strict") + + local_file = self._module.params["local_file"] + try: + with open(local_file, "rb") as f: + filecontent = f.read() + except (OSError, IOError) as exc: + self._module.fail_json("Error reading the file: {0}".format(to_text(exc))) + + filecontent = to_bytes(filecontent, errors="surrogate_or_strict") + local_filehash = hashlib.md5(filecontent).hexdigest() + + decoded_rhash = remote_filehash.decode("UTF-8") + + if local_filehash == decoded_rhash: + return True + else: + return False + + def remote_file_exists(self, remote_file, file_system): + command = "dir {0}/{1}".format(file_system, remote_file) + body = self._connection.run_commands(command)[0] + + if "No such file" in body: + return False + else: + return self.md5sum_check(remote_file, file_system) + + def get_flash_size(self, file_system): + command = "dir {0}".format(file_system) + body = self._connection.run_commands(command)[0] + + match = re.search(r"(\d+) bytes free", body) + if match: + bytes_free = match.group(1) + return int(bytes_free) + + match = re.search(r"No such file or directory", body) + if match: + self._module.fail_json("Invalid nxos filesystem {0}".format(file_system)) + else: + self._module.fail_json("Unable to determine size of filesystem {0}".format(file_system)) + + def enough_space(self, file, file_system): + flash_size = self.get_flash_size(file_system) + file_size = os.path.getsize(file) + if file_size > flash_size: + return False + + return True + + def transfer_file_to_device(self, remote_file): + local_file = self._module.params["local_file"] + file_system = self._module.params["file_system"] + + if not self.enough_space(local_file, file_system): + self._module.fail_json("Could not transfer file. Not enough space on device.") + + # frp = full_remote_path, flp = full_local_path + frp = remote_file + if not file_system.startswith("bootflash:"): + frp = "{0}{1}".format(file_system, remote_file) + flp = os.path.join(os.path.abspath(local_file)) + + try: + self._connection.copy_file( + source=flp, + destination=frp, + proto="scp", + timeout=self._connection.get_option("persistent_command_timeout"), + ) + self.result["transfer_status"] = "Sent: File copied to remote device." + except Exception as exc: + self.result["failed"] = True + self.result["msg"] = "Exception received : %s" % exc + + def run(self): + local_file = self._module.params["local_file"] + remote_file = self._module.params["remote_file"] or os.path.basename(local_file) + file_system = self._module.params["file_system"] + + if not os.path.isfile(local_file): + self._module.fail_json("Local file {0} not found".format(local_file)) + + remote_file = remote_file or os.path.basename(local_file) + remote_exists = self.remote_file_exists(remote_file, file_system) + + if not remote_exists: + self.result["changed"] = True + file_exists = False + else: + self.result["transfer_status"] = "No Transfer: File already copied to remote device." + file_exists = True + + if not self._module.check_mode and not file_exists: + self.transfer_file_to_device(remote_file) + + self.result["local_file"] = local_file + if remote_file is None: + remote_file = os.path.basename(local_file) + self.result["remote_file"] = remote_file + self.result["file_system"] = file_system + + return self.result + + +class FilePull(FileCopy): + def __init__(self, module): + super(FilePull, self).__init__(module) + self.result = {} + + def mkdir(self, directory): + local_dir_root = "/" + dir_array = directory.split("/") + for each in dir_array: + if each: + mkdir_cmd = "mkdir " + local_dir_root + each + self._connection.run_commands(mkdir_cmd) + local_dir_root += each + "/" + return local_dir_root + + def copy_file_from_remote(self, local, local_file_directory, file_system): + # Build copy command components that will be used to initiate copy from the nxos device. + cmdroot = "copy " + self._module.params["file_pull_protocol"] + "://" + ruser = self._module.params["remote_scp_server_user"] + "@" + rserver = self._module.params["remote_scp_server"] + rserverpassword = self._module.params["remote_scp_server_password"] + rfile = self._module.params["remote_file"] + " " + if not rfile.startswith("/"): + rfile = "/" + rfile + + if not self._platform.startswith("DS-") and "MDS" not in self._model: + vrf = " vrf " + self._module.params["vrf"] + else: + vrf = "" + if self._module.params["file_pull_compact"]: + compact = " compact " + else: + compact = "" + if self._module.params["file_pull_kstack"]: + kstack = " use-kstack " + else: + kstack = "" + + # Create local file directory under NX-OS filesystem if + # local_file_directory playbook parameter is set. + local_dir_root = "/" + if local_file_directory: + local_dir_root = self.mkdir(local_file_directory) + + copy_cmd = ( + cmdroot + + ruser + + rserver + + rfile + + file_system + + local_dir_root + + local + + compact + + vrf + + kstack + ) + + self.result["copy_cmd"] = copy_cmd + pulled = self._connection.pull_file(command=copy_cmd, remotepassword=rserverpassword) + if pulled: + self.result[ + "transfer_status" + ] = "Received: File copied/pulled to nxos device from remote scp server." + else: + self.result["failed"] = True + + def run(self): + self.result["failed"] = False + remote_file = self._module.params["remote_file"] + local_file = self._module.params["local_file"] or remote_file.split("/")[-1] + file_system = self._module.params["file_system"] + # Note: This is the local file directory on the remote nxos device. + local_file_dir = self._module.params["local_file_directory"] + + if not self._module.check_mode: + self.copy_file_from_remote(local_file, local_file_dir, file_system) + + self.result["remote_file"] = remote_file + if local_file_dir: + dir = local_file_dir + else: + dir = "" + self.result["local_file"] = file_system + dir + "/" + local_file + self.result["remote_scp_server"] = self._module.params["remote_scp_server"] + self.result["file_system"] = self._module.params["file_system"] + + if not self.result["failed"]: + self.result["changed"] = True + + return self.result + + +def main(): + argument_spec = dict( + vrf=dict(type="str", default="management"), + connect_ssh_port=dict(type="int", default=22), + file_system=dict(type="str", default="bootflash:"), + file_pull=dict(type="bool", default=False), + file_pull_timeout=dict(type="int", default=300), + file_pull_protocol=dict( + type="str", + default="scp", + choices=["scp", "sftp", "http", "https", "tftp", "ftp"], + ), + file_pull_compact=dict(type="bool", default=False), + file_pull_kstack=dict(type="bool", default=False), + local_file=dict(type="path"), + local_file_directory=dict(type="path"), + remote_file=dict(type="path"), + remote_scp_server=dict(type="str"), + remote_scp_server_user=dict(type="str"), + remote_scp_server_password=dict(no_log=True), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=[("file_pull", True, ("remote_file", "remote_scp_server"))], + required_together=[("remote_scp_server", "remote_scp_server_user")], + supports_check_mode=True, + ) + + file_pull = module.params["file_pull"] + + warnings = list() + + if file_pull: + result = FilePull(module).run() + else: + result = FilePush(module).run() + + result["warnings"] = warnings + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_gir.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_gir.py new file mode 100644 index 00000000..c473ff5a --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_gir.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_gir +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Trigger a graceful removal or insertion (GIR) of the switch. +description: +- Trigger a graceful removal or insertion (GIR) of the switch. +- GIR processing may take more than 2 minutes. Timeout settings are automatically + extended to 200s when user timeout settings are insufficient. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state) has effect only in combination with C(system_mode_maintenance_timeout) + or C(system_mode_maintenance_on_reload_reset_reason). +- Using C(system_mode_maintenance) and C(system_mode_maintenance_dont_generate_profile) + would make the module fail, but the system mode will be triggered anyway. +options: + system_mode_maintenance: + description: + - When C(system_mode_maintenance=true) it puts all enabled protocols in maintenance + mode (using the isolate command). When C(system_mode_maintenance=false) it puts + all enabled protocols in normal mode (using the no isolate command). + type: bool + system_mode_maintenance_dont_generate_profile: + description: + - When C(system_mode_maintenance_dont_generate_profile=true) it prevents the dynamic + searching of enabled protocols and executes commands configured in a maintenance-mode + profile. Use this option if you want the system to use a maintenance-mode profile + that you have created. When C(system_mode_maintenance_dont_generate_profile=false) + it prevents the dynamic searching of enabled protocols and executes commands + configured in a normal-mode profile. Use this option if you want the system + to use a normal-mode profile that you have created. + type: bool + system_mode_maintenance_timeout: + description: + - Keeps the switch in maintenance mode for a specified number of minutes. Range + is 5-65535. + type: str + system_mode_maintenance_shutdown: + description: + - Shuts down all protocols, vPC domains, and interfaces except the management + interface (using the shutdown command). This option is disruptive while C(system_mode_maintenance) + (which uses the isolate command) is not. + type: bool + system_mode_maintenance_on_reload_reset_reason: + description: + - Boots the switch into maintenance mode automatically in the event of a specified + system crash. Note that not all reset reasons are applicable for all platforms. + Also if reset reason is set to match_any, it is not idempotent as it turns on + all reset reasons. If reset reason is match_any and state is absent, it turns + off all the reset reasons. + choices: + - hw_error + - svc_failure + - kern_failure + - wdog_timeout + - fatal_error + - lc_failure + - match_any + - manual_reload + - any_other + - maintenance + type: str + state: + description: + - Specify desired state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# Trigger system maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance: true + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Trigger system normal mode +- cisco.nxos.nxos_gir: + system_mode_maintenance: false + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Configure on-reload reset-reason for maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance_on_reload_reset_reason: manual_reload + state: present + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Add on-reload reset-reason for maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance_on_reload_reset_reason: hw_error + state: present + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Remove on-reload reset-reason for maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance_on_reload_reset_reason: manual_reload + state: absent + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Set timeout for maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance_timeout: 30 + state: present + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +# Remove timeout for maintenance mode +- cisco.nxos.nxos_gir: + system_mode_maintenance_timeout: 30 + state: absent + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +final_system_mode: + description: describe the last system mode + returned: verbose mode + type: str + sample: normal +updates: + description: commands sent to the device + returned: verbose mode + type: list + sample: ["terminal dont-ask", "system mode maintenance timeout 10"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def get_system_mode(module): + command = {"command": "show system mode", "output": "text"} + body = run_commands(module, [command])[0] + if body and "normal" in body.lower(): + mode = "normal" + else: + mode = "maintenance" + return mode + + +def get_maintenance_timeout(module): + command = {"command": "show maintenance timeout", "output": "text"} + body = run_commands(module, [command])[0] + timeout = body.split()[4] + return timeout + + +def get_reset_reasons(module): + command = { + "command": "show maintenance on-reload reset-reasons", + "output": "text", + } + body = run_commands(module, [command])[0] + return body + + +def get_commands(module, state, mode): + commands = list() + if module.params["system_mode_maintenance"] is True and mode == "normal": + commands.append("system mode maintenance") + elif module.params["system_mode_maintenance"] is False and mode == "maintenance": + commands.append("no system mode maintenance") + + elif ( + module.params["system_mode_maintenance_dont_generate_profile"] is True and mode == "normal" + ): + commands.append("system mode maintenance dont-generate-profile") + elif ( + module.params["system_mode_maintenance_dont_generate_profile"] is False + and mode == "maintenance" + ): + commands.append("no system mode maintenance dont-generate-profile") + + elif module.params["system_mode_maintenance_timeout"]: + timeout = get_maintenance_timeout(module) + if state == "present" and timeout != module.params["system_mode_maintenance_timeout"]: + commands.append( + "system mode maintenance timeout {0}".format( + module.params["system_mode_maintenance_timeout"], + ), + ) + elif state == "absent" and timeout == module.params["system_mode_maintenance_timeout"]: + commands.append( + "no system mode maintenance timeout {0}".format( + module.params["system_mode_maintenance_timeout"], + ), + ) + + elif module.params["system_mode_maintenance_shutdown"] and mode == "normal": + commands.append("system mode maintenance shutdown") + elif module.params["system_mode_maintenance_shutdown"] is False and mode == "maintenance": + commands.append("no system mode maintenance") + + elif module.params["system_mode_maintenance_on_reload_reset_reason"]: + reset_reasons = get_reset_reasons(module) + if ( + state == "present" + and module.params["system_mode_maintenance_on_reload_reset_reason"].lower() + not in reset_reasons.lower() + ): + commands.append( + "system mode maintenance on-reload " + "reset-reason {0}".format( + module.params["system_mode_maintenance_on_reload_reset_reason"], + ), + ) + elif ( + state == "absent" + and module.params["system_mode_maintenance_on_reload_reset_reason"].lower() + in reset_reasons.lower() + ): + commands.append( + "no system mode maintenance on-reload " + "reset-reason {0}".format( + module.params["system_mode_maintenance_on_reload_reset_reason"], + ), + ) + + if commands: + commands.insert(0, "terminal dont-ask") + return commands + + +def main(): + argument_spec = dict( + system_mode_maintenance=dict(required=False, type="bool"), + system_mode_maintenance_dont_generate_profile=dict(required=False, type="bool"), + system_mode_maintenance_timeout=dict(required=False, type="str"), + system_mode_maintenance_shutdown=dict(required=False, type="bool"), + system_mode_maintenance_on_reload_reset_reason=dict( + required=False, + choices=[ + "hw_error", + "svc_failure", + "kern_failure", + "wdog_timeout", + "fatal_error", + "lc_failure", + "match_any", + "manual_reload", + "any_other", + "maintenance", + ], + ), + state=dict(choices=["absent", "present"], default="present", required=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + [ + "system_mode_maintenance", + "system_mode_maintenance_dont_generate_profile", + "system_mode_maintenance_timeout", + "system_mode_maintenance_shutdown", + "system_mode_maintenance_on_reload_reset_reason", + ], + ], + required_one_of=[ + [ + "system_mode_maintenance", + "system_mode_maintenance_dont_generate_profile", + "system_mode_maintenance_timeout", + "system_mode_maintenance_shutdown", + "system_mode_maintenance_on_reload_reset_reason", + ], + ], + supports_check_mode=True, + ) + + warnings = list() + + state = module.params["state"] + mode = get_system_mode(module) + commands = get_commands(module, state, mode) + changed = False + if commands: + if module.check_mode: + module.exit_json(changed=True, commands=commands) + else: + load_config(module, commands) + changed = True + + result = {} + result["changed"] = changed + if module._verbosity > 0: + final_system_mode = get_system_mode(module) + result["final_system_mode"] = final_system_mode + result["updates"] = commands + + result["warnings"] = warnings + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_gir_profile_management.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_gir_profile_management.py new file mode 100644 index 00000000..84cfc145 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_gir_profile_management.py @@ -0,0 +1,214 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_gir_profile_management +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Create a maintenance-mode or normal-mode profile for GIR. +description: +- Manage a maintenance-mode or normal-mode profile with configuration commands that + can be applied during graceful removal or graceful insertion. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) removes the whole profile. +options: + commands: + description: + - List of commands to be included into the profile. + type: list + elements: str + mode: + description: + - Configure the profile as Maintenance or Normal mode. + required: true + choices: + - maintenance + - normal + type: str + state: + description: + - Specify desired state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# Create a maintenance-mode profile +- cisco.nxos.nxos_gir_profile_management: + mode: maintenance + commands: + - router eigrp 11 + - isolate + +# Remove the maintenance-mode profile +- cisco.nxos.nxos_gir_profile_management: + mode: maintenance + state: absent +""" + +RETURN = """ +proposed: + description: list of commands passed into module. + returned: verbose mode + type: list + sample: ["router eigrp 11", "isolate"] +existing: + description: list of existing profile commands. + returned: verbose mode + type: list + sample: ["router bgp 65535","isolate","router eigrp 10","isolate", + "diagnostic bootup level complete"] +end_state: + description: list of profile entries after module execution. + returned: verbose mode + type: list + sample: ["router bgp 65535","isolate","router eigrp 10","isolate", + "diagnostic bootup level complete","router eigrp 11", "isolate"] +updates: + description: commands sent to the device + returned: always + type: list + sample: ["configure maintenance profile maintenance-mode", + "router eigrp 11","isolate"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +def get_existing(module): + existing = [] + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + + if module.params["mode"] == "maintenance": + parents = ["configure maintenance profile maintenance-mode"] + else: + parents = ["configure maintenance profile normal-mode"] + + config = netcfg.get_section(parents) + if config: + existing = config.splitlines() + existing = [cmd.strip() for cmd in existing] + existing.pop(0) + + return existing + + +def state_present(module, existing, commands): + cmds = list() + if existing == commands: + # Idempotent case + return cmds + cmds.extend(commands) + if module.params["mode"] == "maintenance": + cmds.insert(0, "configure maintenance profile maintenance-mode") + else: + cmds.insert(0, "configure maintenance profile normal-mode") + + return cmds + + +def state_absent(module, existing, commands): + if module.params["mode"] == "maintenance": + cmds = ["no configure maintenance profile maintenance-mode"] + else: + cmds = ["no configure maintenance profile normal-mode"] + return cmds + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def main(): + argument_spec = dict( + commands=dict(required=False, type="list", elements="str"), + mode=dict(required=True, choices=["maintenance", "normal"]), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + state = module.params["state"] + commands = module.params["commands"] or [] + + if state == "absent" and commands: + module.fail_json(msg="when state is absent, no command can be used.") + + existing = invoke("get_existing", module) + end_state = existing + changed = False + + result = {} + cmds = [] + if state == "present" or (state == "absent" and existing): + cmds = invoke("state_%s" % state, module, existing, commands) + + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + if cmds: + load_config(module, cmds) + changed = True + end_state = invoke("get_existing", module) + + result["changed"] = changed + if module._verbosity > 0: + end_state = invoke("get_existing", module) + result["end_state"] = end_state + result["existing"] = existing + result["proposed"] = commands + result["updates"] = cmds + + result["warnings"] = warnings + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_hostname.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_hostname.py new file mode 100644 index 00000000..42e45677 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_hostname.py @@ -0,0 +1,229 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2022 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_hostname +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_hostname +short_description: Hostname resource module. +description: +- This module manages hostname configuration on devices running Cisco NX-OS. +version_added: 2.9.0 +notes: +- Tested against NX-OS 9.3.6. +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section hostname). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of hostname configuration. + type: dict + suboptions: + hostname: + description: Hostname of the device. + type: str + state: + description: + - The state the configuration should be left in. + - The states I(merged), I(replaced) and I(overridden) have identical + behaviour for this module. + - Refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" + +EXAMPLES = """ +# Using merged (replaced, overridden has the same behaviour) + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config | section ^hostname +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_hostname: + config: + hostname: NXOSv-9k + +# Task output +# ------------- +# before: {} +# +# commands: +# - hostname NXOSv-9k +# +# after: +# hostname: NXOSv-9k + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section ^hostname +# hostname NXOSv-9k +# + +# Using deleted + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section ^hostname +# hostname NXOSv-9k + +- name: Delete hostname from running-config + cisco.nxos.nxos_hostname: + state: deleted + +# Task output +# ------------- +# before: +# hostname: NXOSv-9k +# +# commands: +# - no hostname NXOSv-9k +# +# after: {} + +# Using gathered + +- name: Gather hostname facts using gathered + cisco.nxos.nxos_hostname: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# hostname: NXOSv-9k + +# Using rendered + +- name: Render platform specific configuration lines (without connecting to the device) + cisco.nxos.nxos_hostname: + config: + hostname: NXOSv-9k + +# Task Output (redacted) +# ----------------------- +# rendered: +# - hostname NXOSv-9k + +# Using parsed + +# parsed.cfg +# ------------ +# hostname NXOSv-9k + +- name: Parse externally provided hostname config + cisco.nxos.nxos_hostname: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# hostname: NXOSv-9k + +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - hostname switch01 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - hostname switch01 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.hostname.hostname import ( + HostnameArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.hostname.hostname import ( + Hostname, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=HostnameArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Hostname(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp.py new file mode 100644 index 00000000..ee59b7dd --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp.py @@ -0,0 +1,506 @@ +#!/usr/bin/python +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = """ +module: nxos_hsrp +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages HSRP configuration on NX-OS switches. +description: +- Manages HSRP configuration on NX-OS switches. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- HSRP feature needs to be enabled first on the system. +- SVIs must exist before using this module. +- Interface must be a L3 port before using this module. +- HSRP cannot be configured on loopback interfaces. +- MD5 authentication is only possible with HSRPv2 while it is ignored if HSRPv1 is + used instead, while it will not raise any error. Here we allow MD5 authentication + only with HSRPv2 in order to enforce better practice. +options: + group: + description: + - HSRP group number. + required: true + type: str + interface: + description: + - Full name of interface that is being managed for HSRP. + required: true + type: str + version: + description: + - HSRP version. + default: '1' + choices: + - '1' + - '2' + type: str + priority: + description: + - HSRP priority or keyword 'default'. + type: str + preempt: + description: + - Enable/Disable preempt. + choices: + - enabled + - disabled + type: str + vip: + description: + - HSRP virtual IP address or keyword 'default' + type: str + auth_string: + description: + - Authentication string. If this needs to be hidden(for md5 type), the string + should be 7 followed by the key string. Otherwise, it can be 0 followed by key + string or just key string (for backward compatibility). For text type, this + should be just be a key string. if this is 'default', authentication is removed. + type: str + auth_type: + description: + - Authentication type. + choices: + - text + - md5 + type: str + state: + description: + - Specify desired state of the resource. + choices: + - present + - absent + default: present + type: str +""" + +EXAMPLES = """ +- name: Ensure HSRP is configured with following params on a SVI + cisco.nxos.nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + +- name: Ensure HSRP is configured with following params on a SVI with clear text authentication + cisco.nxos.nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + auth_type: text + auth_string: CISCO + +- name: Ensure HSRP is configured with md5 authentication and clear authentication + string + cisco.nxos.nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + auth_type: md5 + auth_string: 0 1234 + +- name: Ensure HSRP is configured with md5 authentication and hidden authentication + string + cisco.nxos.nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + auth_type: md5 + auth_string: 7 1234 + +- name: Remove HSRP config for given interface, group, and VIP + cisco.nxos.nxos_hsrp: + group: 10 + interface: vlan10 + vip: 10.1.1.1 + state: absent +""" + +RETURN = r""" +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface vlan10", "hsrp version 2", "hsrp 30", "ip 10.30.1.1"] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_interface_type, + load_config, + run_commands, +) + + +PARAM_TO_DEFAULT_KEYMAP = { + "vip": None, + "priority": "100", + "auth_type": "text", + "auth_string": "cisco", +} + + +def apply_key_map(key_map, table): + new_dict = {} + for key in table: + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = str(value) + else: + new_dict[new_key] = value + return new_dict + + +def get_interface_mode(interface, intf_type, module): + command = "show interface {0} | json".format(interface) + interface = {} + mode = "unknown" + try: + body = run_commands(module, [command])[0] + except IndexError: + return None + + if intf_type in ["ethernet", "portchannel"]: + interface_table = body["TABLE_interface"]["ROW_interface"] + mode = str(interface_table.get("eth_mode", "layer3")) + if mode == "access" or mode == "trunk": + mode = "layer2" + elif intf_type == "svi": + mode = "layer3" + return mode + + +def get_hsrp_group(group, interface, module): + command = "show hsrp group {0} all | json".format(group) + hsrp = {} + + hsrp_key = { + "sh_if_index": "interface", + "sh_group_num": "group", + "sh_group_version": "version", + "sh_cfg_prio": "priority", + "sh_preempt": "preempt", + "sh_vip": "vip", + "sh_authentication_type": "auth_type", + "sh_keystring_attr": "auth_enc", + "sh_authentication_data": "auth_string", + } + + try: + body = run_commands(module, [command])[0] + hsrp_table = body["TABLE_grp_detail"]["ROW_grp_detail"] + if "sh_keystring_attr" not in hsrp_table: + del hsrp_key["sh_keystring_attr"] + if "unknown enum:" in str(hsrp_table): + hsrp_table = get_hsrp_group_unknown_enum(module, command, hsrp_table) + except (AttributeError, IndexError, TypeError, KeyError): + return {} + + if isinstance(hsrp_table, dict): + hsrp_table = [hsrp_table] + + for hsrp_group in hsrp_table: + parsed_hsrp = apply_key_map(hsrp_key, hsrp_group) + + parsed_hsrp["interface"] = parsed_hsrp["interface"].lower() + + if parsed_hsrp["version"] == "v1": + parsed_hsrp["version"] = "1" + elif parsed_hsrp["version"] == "v2": + parsed_hsrp["version"] = "2" + + if parsed_hsrp["auth_type"] == "md5": + if parsed_hsrp["auth_enc"] == "hidden": + parsed_hsrp["auth_enc"] = "7" + else: + parsed_hsrp["auth_enc"] = "0" + + if parsed_hsrp["interface"] == interface: + return parsed_hsrp + + return hsrp + + +def get_hsrp_group_unknown_enum(module, command, hsrp_table): + """Some older NXOS images fail to set the attr values when using structured output and + instead set the values to <unknown enum>. This fallback method is a workaround that + uses an unstructured (text) request to query the device a second time. + 'sh_preempt' is currently the only attr affected. Add checks for other attrs as needed. + """ + if "unknown enum:" in hsrp_table["sh_preempt"]: + cmd = {"output": "text", "command": command.split("|")[0]} + out = run_commands(module, cmd)[0] + hsrp_table["sh_preempt"] = "enabled" if ("may preempt" in out) else "disabled" + return hsrp_table + + +def get_commands_remove_hsrp(group, interface): + commands = ["interface {0}".format(interface), "no hsrp {0}".format(group)] + return commands + + +def get_commands_config_hsrp(delta, interface, args, existing): + commands = [] + + config_args = { + "group": "hsrp {group}", + "priority": "{priority}", + "preempt": "{preempt}", + "vip": "{vip}", + } + + preempt = delta.get("preempt", None) + group = delta.get("group", None) + vip = delta.get("vip", None) + priority = delta.get("priority", None) + + if preempt: + if preempt == "enabled": + delta["preempt"] = "preempt" + elif preempt == "disabled": + delta["preempt"] = "no preempt" + + if priority: + if priority == "default": + if existing and existing.get("priority") != PARAM_TO_DEFAULT_KEYMAP.get("priority"): + delta["priority"] = "no priority" + else: + del delta["priority"] + else: + delta["priority"] = "priority {0}".format(delta["priority"]) + + if vip: + if vip == "default": + if existing and existing.get("vip") != PARAM_TO_DEFAULT_KEYMAP.get("vip"): + delta["vip"] = "no ip" + else: + del delta["vip"] + else: + delta["vip"] = "ip {0}".format(delta["vip"]) + + for key in delta: + command = config_args.get(key, "DNE").format(**delta) + if command and command != "DNE": + if key == "group": + commands.insert(0, command) + else: + commands.append(command) + command = None + + auth_type = delta.get("auth_type", None) + auth_string = delta.get("auth_string", None) + auth_enc = delta.get("auth_enc", None) + if auth_type or auth_string: + if not auth_type: + auth_type = args["auth_type"] + elif not auth_string: + auth_string = args["auth_string"] + if auth_string != "default": + if auth_type == "md5": + command = "authentication md5 key-string {0} {1}".format(auth_enc, auth_string) + commands.append(command) + elif auth_type == "text": + command = "authentication text {0}".format(auth_string) + commands.append(command) + else: + if existing and existing.get("auth_string") != PARAM_TO_DEFAULT_KEYMAP.get( + "auth_string", + ): + commands.append("no authentication") + + if commands and not group: + commands.insert(0, "hsrp {0}".format(args["group"])) + + version = delta.get("version", None) + if version: + if version == "2": + command = "hsrp version 2" + elif version == "1": + command = "hsrp version 1" + commands.insert(0, command) + commands.insert(0, "interface {0}".format(interface)) + + if commands: + if not commands[0].startswith("interface"): + commands.insert(0, "interface {0}".format(interface)) + + return commands + + +def is_default(interface, module): + command = "show run interface {0}".format(interface) + + try: + body = run_commands(module, [command], check_rc=False)[0] + if "invalid" in body.lower(): + return "DNE" + else: + raw_list = body.split("\n") + if raw_list[-1].startswith("interface"): + return True + else: + return False + except KeyError: + return "DNE" + + +def validate_config(body, vip, module): + new_body = "".join(body) + if "invalid ip address" in new_body.lower(): + module.fail_json(msg="Invalid VIP. Possible duplicate IP address.", vip=vip) + + +def main(): + argument_spec = dict( + group=dict(required=True, type="str"), + interface=dict(required=True), + version=dict(choices=["1", "2"], default="1", required=False), + priority=dict(type="str", required=False), + preempt=dict(type="str", choices=["disabled", "enabled"], required=False), + vip=dict(type="str", required=False), + auth_type=dict(choices=["text", "md5"], required=False), + auth_string=dict(type="str", required=False), + state=dict(choices=["absent", "present"], required=False, default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = dict(changed=False, warnings=warnings) + + interface = module.params["interface"].lower() + group = module.params["group"] + version = module.params["version"] + state = module.params["state"] + priority = module.params["priority"] + preempt = module.params["preempt"] + vip = module.params["vip"] + auth_type = module.params["auth_type"] + auth_full_string = module.params["auth_string"] + auth_enc = "0" + auth_string = None + if auth_full_string: + kstr = auth_full_string.split() + if len(kstr) == 2: + auth_enc = kstr[0] + auth_string = kstr[1] + elif len(kstr) == 1: + auth_string = kstr[0] + else: + module.fail_json(msg="Invalid auth_string") + if auth_enc != "0" and auth_enc != "7": + module.fail_json(msg="Invalid auth_string, only 0 or 7 allowed") + + device_info = get_capabilities(module) + network_api = device_info.get("network_api", "nxapi") + + intf_type = get_interface_type(interface) + if intf_type != "ethernet" and network_api == "cliconf": + if is_default(interface, module) == "DNE": + module.fail_json( + msg="That interface does not exist yet. Create " "it first.", + interface=interface, + ) + if intf_type == "loopback": + module.fail_json( + msg="Loopback interfaces don't support HSRP.", + interface=interface, + ) + + mode = get_interface_mode(interface, intf_type, module) + if mode == "layer2": + module.fail_json( + msg="That interface is a layer2 port.\nMake it " "a layer 3 port first.", + interface=interface, + ) + + if auth_type or auth_string: + if not (auth_type and auth_string): + module.fail_json( + msg="When using auth parameters, you need BOTH " "auth_type AND auth_string.", + ) + + args = dict( + group=group, + version=version, + priority=priority, + preempt=preempt, + vip=vip, + auth_type=auth_type, + auth_string=auth_string, + auth_enc=auth_enc, + ) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + + existing = get_hsrp_group(group, interface, module) + + # This will enforce better practice with md5 and hsrp version. + if proposed.get("auth_type", None) == "md5": + if proposed["version"] == "1": + module.fail_json(msg="It's recommended to use HSRP v2 " "when auth_type=md5") + + elif not proposed.get("auth_type", None) and existing: + if (proposed["version"] == "1" and existing["auth_type"] == "md5") and state == "present": + module.fail_json( + msg="Existing auth_type is md5. It's recommended " "to use HSRP v2 when using md5", + ) + + commands = [] + if state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = get_commands_config_hsrp(delta, interface, args, existing) + commands.extend(command) + + elif state == "absent": + if existing: + command = get_commands_remove_hsrp(group, interface) + commands.extend(command) + + if commands: + if module.check_mode: + module.exit_json(**results) + else: + load_config(module, commands) + + # validate IP + if network_api == "cliconf" and state == "present": + commands.insert(0, "config t") + body = run_commands(module, commands) + validate_config(body, vip, module) + + results["changed"] = True + + if "configure" in commands: + commands.pop(0) + + results["commands"] = commands + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp_interfaces.py new file mode 100644 index 00000000..e5ac6737 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_hsrp_interfaces.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_hsrp_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_hsrp_interfaces +short_description: HSRP interfaces resource module +description: Manages Hot Standby Router Protocol (HSRP) interface attributes. +version_added: 1.0.0 +author: Chris Van Heuveln (@chrisvanheuveln) +notes: +- Tested against NX-OS 7.0(3)I5(1). +- Feature bfd should be enabled for this module. +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^interface'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: The provided configuration + type: list + elements: dict + suboptions: + name: + type: str + description: The name of the interface. + bfd: + type: str + description: + - Enable/Disable HSRP Bidirectional Forwarding Detection (BFD) on the interface. + choices: + - enable + - disable + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using deleted + +- name: Configure hsrp attributes on interfaces + cisco.nxos.nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + - name: Ethernet1/2 + operation: deleted + + +# Using merged + +- name: Configure hsrp attributes on interfaces + cisco.nxos.nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: disable + operation: merged + + +# Using overridden + +- name: Configure hsrp attributes on interfaces + cisco.nxos.nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: disable + operation: overridden + + +# Using replaced + +- name: Configure hsrp attributes on interfaces + cisco.nxos.nxos_hsrp_interfaces: + config: + - name: Ethernet1/1 + bfd: enable + - name: Ethernet1/2 + bfd: disable + operation: replaced + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_hsrp_interfaces: + config: + - name: Ethernet1/800 + bfd: enable + - name: Ethernet1/801 + bfd: enable + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/800" +# - "hsrp bfd" +# - "interface Ethernet1/801" +# - "hsrp bfd" + +# Using parsed + +# parsed.cfg +# ------------ +# interface Ethernet1/800 +# no switchport +# hsrp bfd +# interface Ethernet1/801 +# no switchport +# hsrp bfd + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_hsrp_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - name: Ethernet1/800 +# bfd: enable +# - name: Ethernet1/801 +# bfd: enable + +# Using gathered + +# Existing device config state +# ------------------------------- + +# interface Ethernet1/1 +# no switchport +# hsrp bfd +# interface Ethernet1/2 +# no switchport +# hsrp bfd +# interface Ethernet1/3 +# no switchport + +- name: Gather hsrp_interfaces facts from the device using nxos_hsrp_interfaces + cisco.nxos.nxos_hsrp_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- + +# gathered: +# - name: Ethernet1/1 +# bfd: enable +# - name: Ethernet1/2 +# bfd: enable + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/1', 'hsrp bfd'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.hsrp_interfaces.hsrp_interfaces import ( + Hsrp_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.hsrp_interfaces.hsrp_interfaces import ( + Hsrp_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Hsrp_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Hsrp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp.py new file mode 100644 index 00000000..93770616 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp.py @@ -0,0 +1,162 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_igmp +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages IGMP global configuration. +description: +- Manages IGMP global configuration configuration settings. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- When C(state=default), all supported params will be reset to a default state. +- If restart is set to true with other params set, the restart will happen last, i.e. + after the configuration takes place. +options: + flush_routes: + description: + - Removes routes when the IGMP process is restarted. By default, routes are not + flushed. + type: bool + enforce_rtr_alert: + description: + - Enables or disables the enforce router alert option check for IGMPv2 and IGMPv3 + packets. + type: bool + restart: + description: + - Restarts the igmp process (using an exec config command). + type: bool + default: False + state: + description: + - Manages desired state of the resource. + default: present + choices: + - present + - default + type: str +""" +EXAMPLES = """ +- name: Default igmp global params (all params except restart) + cisco.nxos.nxos_igmp: + state: default + +- name: Ensure the following igmp global config exists on the device + cisco.nxos.nxos_igmp: + flush_routes: true + enforce_rtr_alert: true + +- name: Restart the igmp process + cisco.nxos.nxos_igmp: + restart: true +""" + +RETURN = """ +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ip igmp flush-routes"] +""" +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def get_current(module): + output = run_commands(module, {"command": "show running-config", "output": "text"}) + return { + "flush_routes": "ip igmp flush-routes" in output[0], + "enforce_rtr_alert": "ip igmp enforce-router-alert" in output[0], + } + + +def get_desired(module): + return { + "flush_routes": module.params["flush_routes"], + "enforce_rtr_alert": module.params["enforce_rtr_alert"], + } + + +def main(): + argument_spec = dict( + flush_routes=dict(type="bool"), + enforce_rtr_alert=dict(type="bool"), + restart=dict(type="bool", default=False), + state=dict(choices=["present", "default"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + current = get_current(module) + desired = get_desired(module) + + state = module.params["state"] + + commands = list() + + if state == "default": + if current["flush_routes"]: + commands.append("no ip igmp flush-routes") + if current["enforce_rtr_alert"]: + commands.append("no ip igmp enforce-router-alert") + + elif state == "present": + ldict = { + "flush_routes": "flush-routes", + "enforce_rtr_alert": "enforce-router-alert", + } + for arg in ["flush_routes", "enforce_rtr_alert"]: + if desired[arg] and not current[arg]: + commands.append("ip igmp {0}".format(ldict.get(arg))) + elif current[arg] and not desired[arg]: + commands.append("no ip igmp {0}".format(ldict.get(arg))) + + result = {"changed": False, "updates": commands, "warnings": warnings} + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + if module.params["restart"]: + cmd = {"command": "restart igmp", "output": "text"} + run_commands(module, cmd) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_interface.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_interface.py new file mode 100644 index 00000000..105dac5e --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_interface.py @@ -0,0 +1,650 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_igmp_interface +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages IGMP interface configuration. +description: +- Manages IGMP interface configuration settings. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- When C(state=default), supported params will be reset to a default state. These + include C(version), C(startup_query_interval), C(startup_query_count), C(robustness), + C(querier_timeout), C(query_mrt), C(query_interval), C(last_member_qrt), C(last_member_query_count), + C(group_timeout), C(report_llg), and C(immediate_leave). +- When C(state=absent), all configs for C(oif_ps), and C(oif_routemap) will be removed. +- PIM must be enabled to use this module. +- This module is for Layer 3 interfaces. +- Route-map check not performed (same as CLI) check when configuring route-map with + 'static-oif' +- If restart is set to true with other params set, the restart will happen last, i.e. + after the configuration takes place. However, 'restart' itself is not idempotent + as it is an action and not configuration. +options: + interface: + description: + - The full interface name for IGMP configuration. e.g. I(Ethernet1/2). + required: true + type: str + version: + description: + - IGMP version. It can be 2 or 3 or keyword 'default'. + choices: + - '2' + - '3' + - default + type: str + startup_query_interval: + description: + - Query interval used when the IGMP process starts up. The range is from 1 to + 18000 or keyword 'default'. The default is 31. + type: str + startup_query_count: + description: + - Query count used when the IGMP process starts up. The range is from 1 to 10 + or keyword 'default'. The default is 2. + type: str + robustness: + description: + - Sets the robustness variable. Values can range from 1 to 7 or keyword 'default'. + The default is 2. + type: str + querier_timeout: + description: + - Sets the querier timeout that the software uses when deciding to take over as + the querier. Values can range from 1 to 65535 seconds or keyword 'default'. + The default is 255 seconds. + type: str + query_mrt: + description: + - Sets the response time advertised in IGMP queries. Values can range from 1 to + 25 seconds or keyword 'default'. The default is 10 seconds. + type: str + query_interval: + description: + - Sets the frequency at which the software sends IGMP host query messages. Values + can range from 1 to 18000 seconds or keyword 'default'. The default is 125 seconds. + type: str + last_member_qrt: + description: + - Sets the query interval waited after sending membership reports before the software + deletes the group state. Values can range from 1 to 25 seconds or keyword 'default'. + The default is 1 second. + type: str + last_member_query_count: + description: + - Sets the number of times that the software sends an IGMP query in response to + a host leave message. Values can range from 1 to 5 or keyword 'default'. The + default is 2. + type: str + group_timeout: + description: + - Sets the group membership timeout for IGMPv2. Values can range from 3 to 65,535 + seconds or keyword 'default'. The default is 260 seconds. + type: str + report_llg: + description: + - Configures report-link-local-groups. Enables sending reports for groups in 224.0.0.0/24. + Reports are always sent for nonlink local groups. By default, reports are not + sent for link local groups. + type: bool + immediate_leave: + description: + - Enables the device to remove the group entry from the multicast routing table + immediately upon receiving a leave message for the group. Use this command to + minimize the leave latency of IGMPv2 group memberships on a given IGMP interface + because the device does not send group-specific queries. The default is disabled. + type: bool + oif_routemap: + description: + - Configure a routemap for static outgoing interface (OIF) or keyword 'default'. + type: str + oif_ps: + description: + - Configure prefixes and sources for static outgoing interface (OIF). This is + a list of dict where each dict has source and prefix defined or just prefix + if source is not needed. The specified values will be configured on the device + and if any previous prefix/sources exist, they will be removed. Keyword 'default' + is also accepted which removes all existing prefix/sources. + type: raw + restart: + description: + - Restart IGMP. This is NOT idempotent as this is action only. + type: bool + default: false + state: + description: + - Manages desired state of the resource. + default: present + choices: + - present + - absent + - default + type: str +""" +EXAMPLES = """ +- cisco.nxos.nxos_igmp_interface: + interface: ethernet1/32 + startup_query_interval: 30 + oif_ps: + - {prefix: 238.2.2.6} + - {source: 192.168.0.1, prefix: 238.2.2.5} + state: present +""" +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"startup_query_count": "30", + "oif_ps": [{'prefix': '238.2.2.6'}, {'source': '192.168.0.1', 'prefix': '238.2.2.5'}]} +existing: + description: k/v pairs of existing igmp_interface configuration + returned: always + type: dict + sample: {"startup_query_count": "2", "oif_ps": []} +end_state: + description: k/v pairs of igmp interface configuration after module execution + returned: always + type: dict + sample: {"startup_query_count": "30", + "oif_ps": [{'prefix': '238.2.2.6'}, {'source': '192.168.0.1', 'prefix': '238.2.2.5'}]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface Ethernet1/32", "ip igmp startup-query-count 30", + "ip igmp static-oif 238.2.2.6", "ip igmp static-oif 238.2.2.5 source 192.168.0.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_interface_type, + load_config, + run_commands, +) + + +def execute_show_command(command, module, command_type="cli_show"): + if command_type == "cli_show_ascii": + cmds = [{"command": command, "output": "text"}] + else: + cmds = [{"command": command, "output": "json"}] + + return run_commands(module, cmds) + + +def get_interface_mode(interface, intf_type, module): + command = "show interface {0}".format(interface) + interface = {} + mode = "unknown" + + if intf_type in ["ethernet", "portchannel"]: + body = execute_show_command(command, module)[0] + interface_table = body["TABLE_interface"]["ROW_interface"] + mode = str(interface_table.get("eth_mode", "layer3")) + if mode == "access" or mode == "trunk": + mode = "layer2" + elif intf_type == "loopback" or intf_type == "svi": + mode = "layer3" + return mode + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = value + else: + new_dict[new_key] = value + return new_dict + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_igmp_interface(module, interface): + command = "show ip igmp interface {0}".format(interface) + igmp = {} + + key_map = { + "IGMPVersion": "version", + "ConfiguredStartupQueryInterval": "startup_query_interval", + "StartupQueryCount": "startup_query_count", + "RobustnessVariable": "robustness", + "ConfiguredQuerierTimeout": "querier_timeout", + "ConfiguredMaxResponseTime": "query_mrt", + "ConfiguredQueryInterval": "query_interval", + "LastMemberMTR": "last_member_qrt", + "LastMemberQueryCount": "last_member_query_count", + "ConfiguredGroupTimeout": "group_timeout", + } + + body = execute_show_command(command, module)[0] + + if body: + if "not running" in body: + return igmp + resource = body["TABLE_vrf"]["ROW_vrf"]["TABLE_if"]["ROW_if"] + igmp = apply_key_map(key_map, resource) + report_llg = str(resource["ReportingForLinkLocal"]).lower() + if report_llg == "true": + igmp["report_llg"] = True + elif report_llg == "false": + igmp["report_llg"] = False + + immediate_leave = str(resource["ImmediateLeave"]).lower() # returns en or dis + if re.search(r"^en|^true|^enabled", immediate_leave): + igmp["immediate_leave"] = True + elif re.search(r"^dis|^false|^disabled", immediate_leave): + igmp["immediate_leave"] = False + + # the next block of code is used to retrieve anything with: + # ip igmp static-oif *** i.e.. could be route-map ROUTEMAP + # or PREFIX source <ip>, etc. + command = "show run interface {0} | inc oif".format(interface) + + body = execute_show_command(command, module, command_type="cli_show_ascii")[0] + + staticoif = [] + if body: + split_body = body.split("\n") + route_map_regex = r".*ip igmp static-oif route-map\s+(?P<route_map>\S+).*" + prefix_source_regex = ( + r".*ip igmp static-oif\s+(?P<prefix>" + r"((\d+.){3}\d+))(\ssource\s" + r"(?P<source>\S+))?.*" + ) + + for line in split_body: + temp = {} + try: + match_route_map = re.match(route_map_regex, line, re.DOTALL) + route_map = match_route_map.groupdict()["route_map"] + except AttributeError: + route_map = "" + + try: + match_prefix_source = re.match(prefix_source_regex, line, re.DOTALL) + prefix_source_group = match_prefix_source.groupdict() + prefix = prefix_source_group["prefix"] + source = prefix_source_group["source"] + except AttributeError: + prefix = "" + source = "" + + if route_map: + temp["route_map"] = route_map + if prefix: + temp["prefix"] = prefix + if source: + temp["source"] = source + if temp: + staticoif.append(temp) + + igmp["oif_routemap"] = None + igmp["oif_prefix_source"] = [] + + if staticoif: + if len(staticoif) == 1 and staticoif[0].get("route_map"): + igmp["oif_routemap"] = staticoif[0]["route_map"] + else: + igmp["oif_prefix_source"] = staticoif + + return igmp + + +def config_igmp_interface(delta, existing, existing_oif_prefix_source): + CMDS = { + "version": "ip igmp version {0}", + "startup_query_interval": "ip igmp startup-query-interval {0}", + "startup_query_count": "ip igmp startup-query-count {0}", + "robustness": "ip igmp robustness-variable {0}", + "querier_timeout": "ip igmp querier-timeout {0}", + "query_mrt": "ip igmp query-max-response-time {0}", + "query_interval": "ip igmp query-interval {0}", + "last_member_qrt": "ip igmp last-member-query-response-time {0}", + "last_member_query_count": "ip igmp last-member-query-count {0}", + "group_timeout": "ip igmp group-timeout {0}", + "report_llg": "ip igmp report-link-local-groups", + "immediate_leave": "ip igmp immediate-leave", + "oif_prefix_source": "ip igmp static-oif {0} source {1} ", + "oif_routemap": "ip igmp static-oif route-map {0}", + "oif_prefix": "ip igmp static-oif {0}", + } + + commands = [] + command = None + def_vals = get_igmp_interface_defaults() + + for key, value in delta.items(): + if key == "oif_ps" and value != "default": + for each in value: + if each in existing_oif_prefix_source: + existing_oif_prefix_source.remove(each) + else: + # add new prefix/sources + pf = each["prefix"] + src = "" + if "source" in each.keys(): + src = each["source"] + if src: + commands.append(CMDS.get("oif_prefix_source").format(pf, src)) + else: + commands.append(CMDS.get("oif_prefix").format(pf)) + if existing_oif_prefix_source: + for each in existing_oif_prefix_source: + # remove stale prefix/sources + pf = each["prefix"] + src = "" + if "source" in each.keys(): + src = each["source"] + if src: + commands.append("no " + CMDS.get("oif_prefix_source").format(pf, src)) + else: + commands.append("no " + CMDS.get("oif_prefix").format(pf)) + elif key == "oif_routemap": + if value == "default": + if existing.get(key): + command = "no " + CMDS.get(key).format(existing.get(key)) + else: + command = CMDS.get(key).format(value) + elif value: + if value == "default": + if def_vals.get(key) != existing.get(key): + command = CMDS.get(key).format(def_vals.get(key)) + else: + command = CMDS.get(key).format(value) + elif not value: + command = "no {0}".format(CMDS.get(key).format(value)) + + if command: + if command not in commands: + commands.append(command) + command = None + + return commands + + +def get_igmp_interface_defaults(): + version = "2" + startup_query_interval = "31" + startup_query_count = "2" + robustness = "2" + querier_timeout = "255" + query_mrt = "10" + query_interval = "125" + last_member_qrt = "1" + last_member_query_count = "2" + group_timeout = "260" + report_llg = False + immediate_leave = False + + args = dict( + version=version, + startup_query_interval=startup_query_interval, + startup_query_count=startup_query_count, + robustness=robustness, + querier_timeout=querier_timeout, + query_mrt=query_mrt, + query_interval=query_interval, + last_member_qrt=last_member_qrt, + last_member_query_count=last_member_query_count, + group_timeout=group_timeout, + report_llg=report_llg, + immediate_leave=immediate_leave, + ) + + default = dict((param, value) for (param, value) in args.items() if value is not None) + + return default + + +def config_default_igmp_interface(existing, delta): + commands = [] + proposed = get_igmp_interface_defaults() + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = config_igmp_interface(delta, existing, existing_oif_prefix_source=None) + + if command: + for each in command: + commands.append(each) + + return commands + + +def config_remove_oif(existing, existing_oif_prefix_source): + commands = [] + command = None + if existing.get("oif_routemap"): + commands.append("no ip igmp static-oif route-map {0}".format(existing.get("oif_routemap"))) + elif existing_oif_prefix_source: + for each in existing_oif_prefix_source: + if each.get("prefix") and each.get("source"): + command = "no ip igmp static-oif {0} source {1} ".format( + each.get("prefix"), + each.get("source"), + ) + elif each.get("prefix"): + command = "no ip igmp static-oif {0}".format(each.get("prefix")) + if command: + commands.append(command) + command = None + + return commands + + +def main(): + argument_spec = dict( + interface=dict(required=True, type="str"), + version=dict(required=False, type="str", choices=["2", "3", "default"]), + startup_query_interval=dict(required=False, type="str"), + startup_query_count=dict(required=False, type="str"), + robustness=dict(required=False, type="str"), + querier_timeout=dict(required=False, type="str"), + query_mrt=dict(required=False, type="str"), + query_interval=dict(required=False, type="str"), + last_member_qrt=dict(required=False, type="str"), + last_member_query_count=dict(required=False, type="str"), + group_timeout=dict(required=False, type="str"), + report_llg=dict(type="bool"), + immediate_leave=dict(type="bool"), + oif_routemap=dict(required=False, type="str"), + oif_ps=dict(required=False, type="raw"), + restart=dict(type="bool", default=False), + state=dict(choices=["present", "absent", "default"], default="present"), + ) + + mutually_exclusive = [("oif_ps", "oif_routemap")] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + + state = module.params["state"] + interface = module.params["interface"] + oif_routemap = module.params["oif_routemap"] + oif_ps = module.params["oif_ps"] + + intf_type = get_interface_type(interface) + if get_interface_mode(interface, intf_type, module) == "layer2": + module.fail_json(msg="this module only works on Layer 3 interfaces") + + existing = get_igmp_interface(module, interface) + existing_copy = existing.copy() + end_state = existing_copy + + if not existing.get("version"): + module.fail_json(msg="pim needs to be enabled on the interface") + + existing_oif_prefix_source = existing.get("oif_prefix_source") + # not json serializable + existing.pop("oif_prefix_source") + + if oif_routemap and existing_oif_prefix_source: + module.fail_json( + msg="Delete static-oif configurations on this " + "interface if you want to use a routemap", + ) + + if oif_ps and existing.get("oif_routemap"): + module.fail_json( + msg="Delete static-oif route-map configuration " + "on this interface if you want to config " + "static entries", + ) + + args = [ + "version", + "startup_query_interval", + "startup_query_count", + "robustness", + "querier_timeout", + "query_mrt", + "query_interval", + "last_member_qrt", + "last_member_query_count", + "group_timeout", + "report_llg", + "immediate_leave", + "oif_routemap", + ] + + changed = False + commands = [] + proposed = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + CANNOT_ABSENT = [ + "version", + "startup_query_interval", + "startup_query_count", + "robustness", + "querier_timeout", + "query_mrt", + "query_interval", + "last_member_qrt", + "last_member_query_count", + "group_timeout", + "report_llg", + "immediate_leave", + ] + + if state == "absent": + for each in CANNOT_ABSENT: + if each in proposed: + module.fail_json( + msg="only params: " "oif_ps, oif_routemap can be used when " "state=absent", + ) + + # delta check for all params except oif_ps + delta = dict(set(proposed.items()).difference(existing.items())) + + if oif_ps: + if oif_ps == "default": + delta["oif_ps"] = [] + else: + delta["oif_ps"] = oif_ps + + if state == "present": + if delta: + command = config_igmp_interface(delta, existing, existing_oif_prefix_source) + if command: + commands.append(command) + + elif state == "default": + command = config_default_igmp_interface(existing, delta) + if command: + commands.append(command) + elif state == "absent": + command = None + if existing.get("oif_routemap") or existing_oif_prefix_source: + command = config_remove_oif(existing, existing_oif_prefix_source) + + if command: + commands.append(command) + + command = config_default_igmp_interface(existing, delta) + if command: + commands.append(command) + + cmds = [] + results = {} + if commands: + commands.insert(0, ["interface {0}".format(interface)]) + cmds = flatten_list(commands) + + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + load_config(module, cmds) + changed = True + end_state = get_igmp_interface(module, interface) + if "configure" in cmds: + cmds.pop(0) + + if module.params["restart"]: + cmd = {"command": "restart igmp", "output": "text"} + run_commands(module, cmd) + + results["proposed"] = proposed + results["existing"] = existing_copy + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + results["end_state"] = end_state + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_snooping.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_snooping.py new file mode 100644 index 00000000..6d14ba17 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_igmp_snooping.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_igmp_snooping +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages IGMP snooping global configuration. +description: +- Manages IGMP snooping global configuration. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- When C(state=default), params will be reset to a default state. +- C(group_timeout) also accepts I(never) as an input. +options: + snooping: + description: + - Enables/disables IGMP snooping on the switch. + type: bool + group_timeout: + description: + - Group membership timeout value for all VLANs on the device. Accepted values + are integer in range 1-10080, I(never) and I(default). + type: str + link_local_grp_supp: + description: + - Global link-local groups suppression. + type: bool + report_supp: + description: + - Global IGMPv1/IGMPv2 Report Suppression. + type: bool + v3_report_supp: + description: + - Global IGMPv3 Report Suppression and Proxy Reporting. + type: bool + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - default + type: str +""" + +EXAMPLES = """ +# ensure igmp snooping params supported in this module are in there default state +- cisco.nxos.nxos_igmp_snooping: + state: default + +# ensure following igmp snooping params are in the desired state +- cisco.nxos.nxos_igmp_snooping: + group_timeout: never + snooping: true + link_local_grp_supp: false + optimize_mcast_flood: false + report_supp: true + v3_report_supp: true +""" + +RETURN = """ +commands: + description: command sent to the device + returned: always + type: list + sample: ["ip igmp snooping link-local-groups-suppression", + "ip igmp snooping group-timeout 50", + "no ip igmp snooping report-suppression", + "no ip igmp snooping v3-report-suppression", + "no ip igmp snooping"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module, output="text"): + command = {"command": command, "output": output} + + return run_commands(module, [command]) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_group_timeout(config): + match = re.search(r" Group timeout configured: (\S+)", config, re.M) + if match: + value = match.group(1) + else: + value = "" + return value + + +def get_igmp_snooping(module): + command = "show ip igmp snooping" + existing = {} + + try: + body = execute_show_command(command, module, output="json")[0] + except IndexError: + body = [] + + if body: + snooping = str(body.get("enabled")).lower() + if "none" in snooping: + snooping = str(body.get("GlobalSnoopEnabled")).lower() + if snooping == "true" or snooping == "enabled" or snooping == "yes": + existing["snooping"] = True + else: + existing["snooping"] = False + + report_supp = str(body.get("grepsup")).lower() + if "none" in report_supp: + report_supp = str(body.get("GlobalReportSupression")).lower() + if report_supp == "true" or report_supp == "enabled": + existing["report_supp"] = True + else: + existing["report_supp"] = False + + link_local_grp_supp = str(body.get("glinklocalgrpsup")).lower() + if "none" in link_local_grp_supp: + link_local_grp_supp = str(body.get("GlobalLinkLocalGroupSupression")).lower() + if link_local_grp_supp == "true" or link_local_grp_supp == "enabled": + existing["link_local_grp_supp"] = True + else: + existing["link_local_grp_supp"] = False + + v3_report_supp = str(body.get("gv3repsup")).lower() + if "none" in v3_report_supp: + v3_report_supp = str(body.get("GlobalV3ReportSupression")).lower() + if v3_report_supp == "true" or v3_report_supp == "enabled": + existing["v3_report_supp"] = True + else: + existing["v3_report_supp"] = False + + command = "show ip igmp snooping" + body = execute_show_command(command, module)[0] + if body: + existing["group_timeout"] = get_group_timeout(body) + + return existing + + +def config_igmp_snooping(delta, existing, default=False): + CMDS = { + "snooping": "ip igmp snooping", + "group_timeout": "ip igmp snooping group-timeout {}", + "link_local_grp_supp": "ip igmp snooping link-local-groups-suppression", + "v3_report_supp": "ip igmp snooping v3-report-suppression", + "report_supp": "ip igmp snooping report-suppression", + } + + commands = [] + command = None + gt_command = None + for key, value in delta.items(): + if value: + if default and key == "group_timeout": + if existing.get(key): + gt_command = "no " + CMDS.get(key).format(existing.get(key)) + elif value == "default" and key == "group_timeout": + if existing.get(key): + command = "no " + CMDS.get(key).format(existing.get(key)) + else: + command = CMDS.get(key).format(value) + else: + command = "no " + CMDS.get(key).format(value) + + if command: + commands.append(command) + command = None + + if gt_command: + # ensure that group-timeout command is configured last + commands.append(gt_command) + return commands + + +def get_igmp_snooping_defaults(): + group_timeout = "dummy" + report_supp = True + link_local_grp_supp = True + v3_report_supp = False + snooping = True + + args = dict( + snooping=snooping, + link_local_grp_supp=link_local_grp_supp, + report_supp=report_supp, + v3_report_supp=v3_report_supp, + group_timeout=group_timeout, + ) + + default = dict((param, value) for (param, value) in args.items() if value is not None) + + return default + + +def igmp_snooping_gt_dependency(command, existing, module): + # group-timeout will fail if igmp snooping is disabled + gt = [i for i in command if i.startswith("ip igmp snooping group-timeout")] + if gt: + if "no ip igmp snooping" in command or ( + existing["snooping"] is False and "ip igmp snooping" not in command + ): + msg = "group-timeout cannot be enabled or changed when ip igmp snooping is disabled" + module.fail_json(msg=msg) + else: + # ensure that group-timeout command is configured last + command.remove(gt[0]) + command.append(gt[0]) + + +def main(): + argument_spec = dict( + snooping=dict(required=False, type="bool"), + group_timeout=dict(required=False, type="str"), + link_local_grp_supp=dict(required=False, type="bool"), + report_supp=dict(required=False, type="bool"), + v3_report_supp=dict(required=False, type="bool"), + state=dict(choices=["present", "default"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + snooping = module.params["snooping"] + link_local_grp_supp = module.params["link_local_grp_supp"] + report_supp = module.params["report_supp"] + v3_report_supp = module.params["v3_report_supp"] + group_timeout = module.params["group_timeout"] + state = module.params["state"] + + args = dict( + snooping=snooping, + link_local_grp_supp=link_local_grp_supp, + report_supp=report_supp, + v3_report_supp=v3_report_supp, + group_timeout=group_timeout, + ) + + proposed = dict((param, value) for (param, value) in args.items() if value is not None) + + existing = get_igmp_snooping(module) + + commands = [] + if state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = config_igmp_snooping(delta, existing) + if command: + if group_timeout: + igmp_snooping_gt_dependency(command, existing, module) + commands.append(command) + elif state == "default": + proposed = get_igmp_snooping_defaults() + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = config_igmp_snooping(delta, existing, default=True) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_install_os.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_install_os.py new file mode 100644 index 00000000..2327ab54 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_install_os.py @@ -0,0 +1,594 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_install_os +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Set boot options like boot, kickstart image and issu. +description: +- Install an operating system by setting the boot options like boot image and kickstart + image and optionally select to install using ISSU (In Server Software Upgrade). +version_added: 1.0.0 +notes: +- Tested against the following platforms and images - N9k 7.0(3)I4(6), 7.0(3)I5(3), + 7.0(3)I6(1), 7.0(3)I7(1), 7.0(3)F2(2), 7.0(3)F3(2) - N3k 6.0(2)A8(6), 6.0(2)A8(8), + 7.0(3)I6(1), 7.0(3)I7(1) - N7k 7.3(0)D1(1), 8.0(1), 8.1(1), 8.2(1) +- Tested against Cisco MDS NX-OS 9.2(1) +- This module requires both the ANSIBLE_PERSISTENT_CONNECT_TIMEOUT and ANSIBLE_PERSISTENT_COMMAND_TIMEOUT + timers to be set to 600 seconds or higher. The module will exit if the timers are + not set properly. +- When using connection local, ANSIBLE_PERSISTENT_CONNECT_TIMEOUT and ANSIBLE_PERSISTENT_COMMAND_TIMEOUT + can only be set using ENV variables or the ansible.cfg file. +- Do not include full file paths, just the name of the file(s) stored on the top level + flash directory. +- This module attempts to install the software immediately, which may trigger a reboot. +- In check mode, the module will indicate if an upgrade is needed and whether or not + the upgrade is disruptive or non-disruptive(ISSU). +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbibo (@GGabriele) +options: + system_image_file: + description: + - Name of the system (or combined) image file on flash. + required: true + type: str + kickstart_image_file: + description: + - Name of the kickstart image file on flash. (Not required on all Nexus platforms) + type: str + issu: + description: + - Upgrade using In Service Software Upgrade (ISSU). (Supported on N5k, N7k, N9k + platforms) + - Selecting 'required' or 'yes' means that upgrades will only proceed if the switch + is capable of ISSU. + - Selecting 'desired' means that upgrades will use ISSU if possible but will fall + back to disruptive upgrade if needed. + - Selecting 'no' means do not use ISSU. Forced disruptive. + choices: + - "required" + - "desired" + - "yes" + - "no" + default: "no" + type: str +""" + +EXAMPLES = """ +- name: Install OS on N9k + check_mode: no + cisco.nxos.nxos_install_os: + system_image_file: nxos.7.0.3.I6.1.bin + issu: desired + +- name: Wait for device to come back up with new image + wait_for: + port: 22 + state: started + timeout: 500 + delay: 60 + host: '{{ inventory_hostname }}' + +- name: Check installed OS for newly installed version + nxos_command: + commands: [show version | json] + register: output +- assert: + that: + - output['stdout'][0]['kickstart_ver_str'] == '7.0(3)I6(1)' +""" + +RETURN = """ +install_state: + description: Boot and install information. + returned: always + type: dict + sample: { + "install_state": [ + "Compatibility check is done:", + "Module bootable Impact Install-type Reason", + "------ -------- -------------- ------------ ------", + " 1 yes non-disruptive reset ", + "Images will be upgraded according to following table:", + "Module Image Running-Version(pri:alt) New-Version Upg-Required", + "------ ---------- ---------------------------------------- -------------------- ------------", + " 1 nxos 7.0(3)I6(1) 7.0(3)I7(1) yes", + " 1 bios v4.4.0(07/12/2017) v4.4.0(07/12/2017) no" + ], + } +""" + + +import re + +from time import sleep + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +# Output options are 'text' or 'json' +def execute_show_command(module, command, output="text"): + cmds = [{"command": command, "output": output}] + + return run_commands(module, cmds) + + +def get_platform(module): + """Determine platform type""" + data = execute_show_command(module, "show inventory", "json") + pid = data[0]["TABLE_inv"]["ROW_inv"][0]["productid"] + + if re.search(r"N3K", pid): + type = "N3K" + elif re.search(r"N5K", pid): + type = "N5K" + elif re.search(r"N6K", pid): + type = "N6K" + elif re.search(r"N7K", pid): + type = "N7K" + elif re.search(r"N9K", pid): + type = "N9K" + else: + type = "unknown" + + return type + + +def parse_show_install(data): + """Helper method to parse the output of the 'show install all impact' or + 'install all' commands. + + Sample Output: + + Installer will perform impact only check. Please wait. + + Verifying image bootflash:/nxos.7.0.3.F2.2.bin for boot variable "nxos". + [####################] 100% -- SUCCESS + + Verifying image type. + [####################] 100% -- SUCCESS + + Preparing "bios" version info using image bootflash:/nxos.7.0.3.F2.2.bin. + [####################] 100% -- SUCCESS + + Preparing "nxos" version info using image bootflash:/nxos.7.0.3.F2.2.bin. + [####################] 100% -- SUCCESS + + Performing module support checks. + [####################] 100% -- SUCCESS + + Notifying services about system upgrade. + [####################] 100% -- SUCCESS + + + + Compatibility check is done: + Module bootable Impact Install-type Reason + ------ -------- -------------- ------------ ------ + 8 yes disruptive reset Incompatible image for ISSU + 21 yes disruptive reset Incompatible image for ISSU + + + Images will be upgraded according to following table: + Module Image Running-Version(pri:alt) New-Version Upg-Required + ------ ---------- ---------------------------------------- ------------ + 8 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes + 8 bios v01.17 v01.17 no + 21 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes + 21 bios v01.70 v01.70 no + """ + if len(data) > 0: + data = massage_install_data(data) + ud = {"raw": data} + ud["processed"] = [] + ud["disruptive"] = False + ud["upgrade_needed"] = False + ud["error"] = False + ud["invalid_command"] = False + ud["install_in_progress"] = False + ud["server_error"] = False + ud["upgrade_succeeded"] = False + ud["use_impact_data"] = False + + # Check for server errors + if isinstance(data, int): + if data == -1: + ud["server_error"] = True + elif data >= 500: + ud["server_error"] = True + elif data == -32603: + ud["server_error"] = True + elif data == 1: + ud["server_error"] = True + return ud + else: + ud["list_data"] = data.split("\n") + + for x in ud["list_data"]: + # Check for errors and exit if found. + if re.search(r"Pre-upgrade check failed", x): + ud["error"] = True + break + if re.search(r"[I|i]nvalid command", x): + ud["invalid_command"] = True + ud["error"] = True + break + if re.search(r"No install all data found", x): + ud["error"] = True + break + + # Check for potentially transient conditions + if re.search(r"Another install procedure may\s*be in progress", x): + ud["install_in_progress"] = True + break + if re.search(r"Backend processing error", x): + ud["server_error"] = True + break + if re.search(r"timed out", x): + ud["server_error"] = True + break + if re.search(r"^(-1|5\d\d)$", x): + ud["server_error"] = True + break + + # Check for messages indicating a successful upgrade. + if re.search(r"Finishing the upgrade", x): + ud["upgrade_succeeded"] = True + break + if re.search(r"Install has been successful", x): + ud["upgrade_succeeded"] = True + break + if re.search(r"Switching over onto standby", x): + ud["upgrade_succeeded"] = True + break + + # We get these messages when the upgrade is non-disruptive and + # we loose connection with the switchover but far enough along that + # we can be confident the upgrade succeeded. + if re.search(r"timeout .*trying to send command: install", x): + ud["upgrade_succeeded"] = True + ud["use_impact_data"] = True + break + if re.search(r"[C|c]onnection failure: timed out", x): + ud["upgrade_succeeded"] = True + ud["use_impact_data"] = True + break + + # Begin normal parsing. + if re.search(r"----|Module|Images will|Compatibility", x): + ud["processed"].append(x) + continue + # Check to see if upgrade will be disruptive or non-disruptive and + # build dictionary of individual modules and their status. + # Sample Line: + # + # Module bootable Impact Install-type Reason + # ------ -------- ---------- ------------ ------ + # 8 yes disruptive reset Incompatible image + rd = r"(\d+)\s+(\S+)\s+(disruptive|non-disruptive)\s+(\S+)" + mo = re.search(rd, x) + if mo: + ud["processed"].append(x) + key = "m%s" % mo.group(1) + field = "disruptive" + if mo.group(3) == "non-disruptive": + ud[key] = {field: False} + else: + ud[field] = True + ud[key] = {field: True} + field = "bootable" + if mo.group(2) == "yes": + ud[key].update({field: True}) + else: + ud[key].update({field: False}) + continue + + # Check to see if switch needs an upgrade and build a dictionary + # of individual modules and their individual upgrade status. + # Sample Line: + # + # Module Image Running-Version(pri:alt) New-Version Upg-Required + # ------ ----- ---------------------------------------- ------------ + # 8 lcn9k 7.0(3)F3(2) 7.0(3)F2(2) yes + mo = re.search(r"(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(yes|no)", x) + if mo: + ud["processed"].append(x) + key = "m%s_%s" % (mo.group(1), mo.group(2)) + field = "upgrade_needed" + if mo.group(5) == "yes": + ud[field] = True + ud[key] = {field: True} + else: + ud[key] = {field: False} + continue + + return ud + + +def massage_install_data(data): + # Transport cli returns a list containing one result item. + # Transport nxapi returns a list containing two items. The second item + # contains the data we are interested in. + default_error_msg = "No install all data found" + if len(data) == 1: + result_data = data[0] + elif len(data) == 2: + result_data = data[1] + else: + result_data = default_error_msg + + # Further processing may be needed for result_data + if len(data) == 2 and isinstance(data[1], dict): + if "clierror" in data[1].keys(): + result_data = data[1]["clierror"] + elif "code" in data[1].keys() and data[1]["code"] == "500": + # We encountered a backend processing error for nxapi + result_data = data[1]["msg"] + else: + result_data = default_error_msg + return result_data + + +def build_install_cmd_set(issu, image, kick, type, force=True): + commands = ["terminal dont-ask"] + + # Different NX-OS platforms behave differently for + # disruptive and non-disruptive upgrade paths. + # + # 1) Combined kickstart/system image: + # * Use option 'non-disruptive' for issu. + # * Omit option 'non-disruptive' for disruptive upgrades. + # 2) Separate kickstart + system images. + # * Omit hidden 'force' option for issu. + # * Use hidden 'force' option for disruptive upgrades. + # * Note: Not supported on all platforms + if re.search(r"required|desired|yes", issu): + if kick is None: + issu_cmd = "non-disruptive" + else: + issu_cmd = "" + else: + if kick is None: + issu_cmd = "" + else: + issu_cmd = "force" if force else "" + + if type == "impact": + rootcmd = "show install all impact" + # The force option is not available for the impact command. + if kick: + issu_cmd = "" + else: + rootcmd = "install all" + if kick is None: + commands.append("%s nxos %s %s" % (rootcmd, image, issu_cmd)) + else: + commands.append("%s %s system %s kickstart %s" % (rootcmd, issu_cmd, image, kick)) + + return commands + + +def parse_show_version(data): + version_data = {"raw": data[0].split("\n")} + version_data["version"] = "" + version_data["error"] = False + for x in version_data["raw"]: + mo = re.search(r"(kickstart|system|NXOS):\s+version\s+(\S+)", x) + if mo: + version_data["version"] = mo.group(2) + continue + + if version_data["version"] == "": + version_data["error"] = True + + return version_data + + +def check_mode_legacy(module, issu, image, kick=None): + """Some platforms/images/transports don't support the 'install all impact' + command so we need to use a different method.""" + current = execute_show_command(module, "show version", "json")[0] + # Call parse_show_data on empty string to create the default upgrade + # data structure dictionary + data = parse_show_install("") + upgrade_msg = "No upgrade required" + + # Process System Image + data["error"] = False + tsver = "show version image bootflash:%s" % image + data["upgrade_cmd"] = [tsver] + target_image = parse_show_version(execute_show_command(module, tsver)) + if target_image["error"]: + data["error"] = True + data["raw"] = target_image["raw"] + if current["kickstart_ver_str"] != target_image["version"] and not data["error"]: + data["upgrade_needed"] = True + data["disruptive"] = True + upgrade_msg = "Switch upgraded: system: %s" % tsver + + # Process Kickstart Image + if kick is not None and not data["error"]: + tkver = "show version image bootflash:%s" % kick + data["upgrade_cmd"].append(tsver) + target_kick = parse_show_version(execute_show_command(module, tkver)) + if target_kick["error"]: + data["error"] = True + data["raw"] = target_kick["raw"] + if current["kickstart_ver_str"] != target_kick["version"] and not data["error"]: + data["upgrade_needed"] = True + data["disruptive"] = True + upgrade_msg = upgrade_msg + " kickstart: %s" % tkver + + data["list_data"] = data["raw"] + data["processed"] = upgrade_msg + return data + + +def check_mode_nextgen(module, issu, image, kick=None): + """Use the 'install all impact' command for check_mode""" + opts = {"ignore_timeout": True} + commands = build_install_cmd_set(issu, image, kick, "impact") + data = parse_show_install(load_config(module, commands, True, opts)) + # If an error is encountered when issu is 'desired' then try again + # but set issu to 'no' + if data["error"] and issu == "desired": + issu = "no" + commands = build_install_cmd_set(issu, image, kick, "impact") + # The system may be busy from the previous call to check_mode so loop + # until it's done. + data = check_install_in_progress(module, commands, opts) + if data["server_error"]: + data["error"] = True + data["upgrade_cmd"] = commands + return data + + +def check_install_in_progress(module, commands, opts): + for attempt in range(20): + data = parse_show_install(load_config(module, commands, True, opts)) + if data["install_in_progress"]: + sleep(1) + continue + break + return data + + +def check_mode(module, issu, image, kick=None): + """Check switch upgrade impact using 'show install all impact' command""" + data = check_mode_nextgen(module, issu, image, kick) + if data["server_error"]: + # We encountered an unrecoverable error in the attempt to get upgrade + # impact data from the 'show install all impact' command. + # Fallback to legacy method. + data = check_mode_legacy(module, issu, image, kick) + if data["invalid_command"]: + # If we are upgrading from a device running a separate kickstart and + # system image the impact command will fail. + # Fallback to legacy method. + data = check_mode_legacy(module, issu, image, kick) + return data + + +def do_install_all(module, issu, image, kick=None): + """Perform the switch upgrade using the 'install all' command""" + impact_data = check_mode(module, issu, image, kick) + if module.check_mode: + # Check mode set in the playbook so just return the impact data. + msg = "*** SWITCH WAS NOT UPGRADED: IMPACT DATA ONLY ***" + impact_data["processed"].append(msg) + return impact_data + if impact_data["error"]: + # Check mode discovered an error so return with this info. + return impact_data + elif not impact_data["upgrade_needed"]: + # The switch is already upgraded. Nothing more to do. + return impact_data + else: + # If we get here, check_mode returned no errors and the switch + # needs to be upgraded. + if impact_data["disruptive"]: + # Check mode indicated that ISSU is not possible so issue the + # upgrade command without the non-disruptive flag unless the + # playbook specified issu: yes/required. + if issu == "yes": + msg = "ISSU/ISSD requested but impact data indicates ISSU/ISSD is not possible" + module.fail_json(msg=msg, raw_data=impact_data["list_data"]) + else: + issu = "no" + + commands = build_install_cmd_set(issu, image, kick, "install") + opts = {"ignore_timeout": True} + # The system may be busy from the call to check_mode so loop until + # it's done. + upgrade = check_install_in_progress(module, commands, opts) + if upgrade["invalid_command"] and "force" in commands[1]: + # Not all platforms support the 'force' keyword. Check for this + # condition and re-try without the 'force' keyword if needed. + commands = build_install_cmd_set(issu, image, kick, "install", False) + upgrade = check_install_in_progress(module, commands, opts) + upgrade["upgrade_cmd"] = commands + + # Special case: If we encounter a server error at this stage + # it means the command was sent and the upgrade was started but + # we will need to use the impact data instead of the current install + # data. + if upgrade["server_error"]: + upgrade["upgrade_succeeded"] = True + upgrade["use_impact_data"] = True + + if upgrade["use_impact_data"]: + if upgrade["upgrade_succeeded"]: + upgrade = impact_data + upgrade["upgrade_succeeded"] = True + else: + upgrade = impact_data + upgrade["upgrade_succeeded"] = False + + if not upgrade["upgrade_succeeded"]: + upgrade["error"] = True + return upgrade + + +def main(): + argument_spec = dict( + system_image_file=dict(required=True), + kickstart_image_file=dict(required=False), + issu=dict(choices=["required", "desired", "no", "yes"], default="no"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + # Get system_image_file(sif), kickstart_image_file(kif) and + # issu settings from module params. + sif = module.params["system_image_file"] + kif = module.params["kickstart_image_file"] + issu = module.params["issu"] + + if re.search(r"(yes|required)", issu): + issu = "yes" + + if kif == "null" or kif == "": + kif = None + + install_result = do_install_all(module, issu, sif, kick=kif) + if install_result["error"]: + cmd = install_result["upgrade_cmd"] + msg = "Failed to upgrade device using command: %s" % cmd + module.fail_json(msg=msg, raw_data=install_result["list_data"]) + + state = install_result["processed"] + changed = install_result["upgrade_needed"] + module.exit_json(changed=changed, install_state=state, warnings=warnings) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_interfaces.py new file mode 100644 index 00000000..f03f746b --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_interfaces.py @@ -0,0 +1,455 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_interfaces +short_description: Interfaces resource module +description: This module manages the interface attributes of NX-OS interfaces. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section ^interface) + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of interface, e.g. Ethernet1/1, port-channel10. + type: str + required: true + description: + description: + - Interface description. + type: str + enabled: + description: + - Administrative state of the interface. Set the value to C(true) to administratively + enable the interface or C(false) to disable it + type: bool + speed: + description: + - Interface link speed. Applicable for Ethernet interfaces only. + type: str + mode: + description: + - Manage Layer2 or Layer3 state of the interface. Applicable for Ethernet + and port channel interfaces only. + choices: + - layer2 + - layer3 + type: str + mtu: + description: + - MTU for a specific interface. Must be an even number between 576 and 9216. + Applicable for Ethernet interfaces only. + type: str + duplex: + description: + - Interface link status. Applicable for Ethernet interfaces only. + type: str + choices: + - full + - half + - auto + ip_forward: + description: + - Enable or disable IP forward feature on SVIs. Set the value to C(true) to + enable or C(false) to disable. + type: bool + fabric_forwarding_anycast_gateway: + description: + - Associate SVI with anycast gateway under VLAN configuration mode. Applicable + for SVI interfaces only. + type: bool + state: + description: + - The state of the configuration after module completion + - The state I(rendered) considers the system default mode for interfaces to be + "Layer 3" and the system default state for interfaces to be shutdown. + - The state I(purged) negates virtual interfaces that are specified in task + from running-config. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + - purged + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# description testing +# mtu 1800 + +- name: Merge provided configuration with device configuration + cisco.nxos.nxos_interfaces: + config: + - name: Ethernet1/1 + description: Configured by Ansible + enabled: true + - name: Ethernet1/2 + description: Configured by Ansible Network + enabled: false + state: merged + +# After state: +# ------------ +# +# interface Ethernet1/1 +# description Configured by Ansible +# no shutdown +# mtu 1800 +# interface Ethernet2 +# description Configured by Ansible Network +# shutdown + + +# Using replaced + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# description Interface 1/1 +# interface Ethernet1/2 + +- name: Replaces device configuration of listed interfaces with provided configuration + cisco.nxos.nxos_interfaces: + config: + - name: Ethernet1/1 + description: Configured by Ansible + enabled: true + mtu: 2000 + - name: Ethernet1/2 + description: Configured by Ansible Network + enabled: false + mode: layer2 + state: replaced + +# After state: +# ------------ +# +# interface Ethernet1/1 +# description Configured by Ansible +# no shutdown +# mtu 1500 +# interface Ethernet2/2 +# description Configured by Ansible Network +# shutdown +# switchport + + +# Using overridden + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# description Interface Ethernet1/1 +# interface Ethernet1/2 +# interface mgmt0 +# description Management interface +# ip address dhcp + +- name: Override device configuration of all interfaces with provided configuration + cisco.nxos.nxos_interfaces: + config: + - name: Ethernet1/1 + enabled: true + - name: Ethernet1/2 + description: Configured by Ansible Network + enabled: false + state: overridden + +# After state: +# ------------ +# +# interface Ethernet1/1 +# interface Ethernet1/2 +# description Configured by Ansible Network +# shutdown +# interface mgmt0 +# ip address dhcp + + +# Using deleted + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# description Interface Ethernet1/1 +# interface Ethernet1/2 +# interface mgmt0 +# description Management interface +# ip address dhcp + +- name: Delete or return interface parameters to default settings + cisco.nxos.nxos_interfaces: + config: + - name: Ethernet1/1 + state: deleted + +# After state: +# ------------ +# +# interface Ethernet1/1 +# interface Ethernet1/2 +# interface mgmt0 +# description Management interface +# ip address dhcp + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_interfaces: + config: + - name: Ethernet1/1 + description: outbound-intf + mode: layer3 + speed: 100 + - name: Ethernet1/2 + mode: layer2 + enabled: true + duplex: full + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/1" +# - "description outbound-intf" +# - "speed 100" +# - "interface Ethernet1/2" +# - "switchport" +# - "duplex full" +# - "no shutdown" + +# Using parsed + +# parsed.cfg +# ------------ +# interface Ethernet1/800 +# description test-1 +# speed 1000 +# shutdown +# no switchport +# duplex half +# interface Ethernet1/801 +# description test-2 +# switchport +# no shutdown +# mtu 1800 + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# - description: "test-1" +# duplex: "half" +# enabled: false +# mode: "layer3" +# name: "Ethernet1/800" +# speed: "1000" +# +# - description: "test-2" +# enabled: true +# mode: "layer2" +# mtu: "1800" +# name: "Ethernet1/801" + +# Using gathered + +# Existing device config state +# ----------------------------- +# interface Ethernet1/1 +# description outbound-intf +# switchport +# no shutdown +# interface Ethernet1/2 +# description intf-l3 +# speed 1000 +# interface Ethernet1/3 +# interface Ethernet1/4 +# interface Ethernet1/5 + +- name: Gather interfaces facts from the device using nxos_interfaces + cisco.nxos.nxos_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- +# - name: Ethernet1/1 +# description: outbound-intf +# mode: layer2 +# enabled: True +# - name: Ethernet1/2 +# description: intf-l3 +# speed: "1000" + +# Using purged + +# Existing device config state +# ----------------------------- +# interface Vlan1 +# interface Vlan42 +# mtu 1800 +# interface port-channel10 +# interface port-channel11 +# interface Ethernet1/1 +# interface Ethernet1/2 +# interface Ethernet1/2.100 +# description sub-intf + +- name: Purge virtual interfaces from running-config + cisco.nxos.nxos_interfaces: + config: + - name: Vlan42 + - name: port-channel10 + - name: Ethernet1/2.100 + state: purged + +# Task output +# ------------ +# before: +# - name: Vlan1 +# - mtu: '1800' +# name: Vlan42 +# - name: port-channel10 +# - name: port-channel11 +# - name: Ethernet1/1 +# - name: Ethernet1/2 +# - description: sub-intf +# name: Ethernet1/2.100 +# +# commands: +# - no interface port-channel10 +# - no interface Ethernet1/2.100 +# - no interface Vlan42 +# +# after: +# - name: Vlan1 +# - name: port-channel11 +# - name: Ethernet1/1 +# - name: Ethernet1/2 +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/1', 'mtu 1800'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.interfaces.interfaces import ( + InterfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.interfaces.interfaces import ( + Interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=InterfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_l2_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_l2_interfaces.py new file mode 100644 index 00000000..3e73c432 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_l2_interfaces.py @@ -0,0 +1,408 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_l2_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_l2_interfaces +short_description: L2 interfaces resource module +description: This module manages Layer-2 interfaces attributes of NX-OS Interfaces. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of Layer-2 interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of interface, i.e. Ethernet1/1. + type: str + required: true + access: + description: + - Switchport mode access command to configure the interface as a Layer-2 access. + type: dict + suboptions: + vlan: + description: + - Configure given VLAN in access port. It's used as the access VLAN ID. + type: int + trunk: + description: + - Switchport mode trunk command to configure the interface as a Layer-2 trunk. + type: dict + suboptions: + native_vlan: + description: + - Native VLAN to be configured in trunk port. It is used as the trunk + native VLAN ID. + type: int + allowed_vlans: + description: + - List of allowed VLANs in a given trunk port. These are the only VLANs + that will be configured on the trunk. + type: str + mode: + description: + - Mode in which interface needs to be configured. + - Access mode is not shown in interface facts, so idempotency will not be + maintained for switchport mode access and every time the output will come + as changed=True. + type: str + choices: + - dot1q-tunnel + - access + - trunk + - fex-fabric + - fabricpath + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# switchport access vlan 20 +# interface Ethernet1/2 +# switchport trunk native vlan 20 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_l2_interfaces: + config: + - name: Ethernet1/1 + trunk: + native_vlan: 10 + allowed_vlans: 2,4,15 + - name: Ethernet1/2 + access: + vlan: 30 + state: merged + +# After state: +# ------------ +# +# interface Ethernet1/1 +# switchport trunk native vlan 10 +# switchport trunk allowed vlans 2,4,15 +# interface Ethernet1/2 +# switchport access vlan 30 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + + +# Using replaced + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# switchport access vlan 20 +# interface Ethernet1/2 +# switchport trunk native vlan 20 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + +- name: Replace device configuration of specified L2 interfaces with provided configuration. + cisco.nxos.nxos_l2_interfaces: + config: + - name: Ethernet1/1 + trunk: + native_vlan: 20 + allowed_vlans: 5-10, 15 + state: replaced + +# After state: +# ------------ +# +# interface Ethernet1/1 +# switchport trunk native vlan 20 +# switchport trunk allowed vlan 5-10,15 +# interface Ethernet1/2 +# switchport trunk native vlan 20 +# switchport mode trunk +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + + +# Using overridden + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# switchport access vlan 20 +# interface Ethernet1/2 +# switchport trunk native vlan 20 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + +- name: Override device configuration of all L2 interfaces on device with provided + configuration. + cisco.nxos.nxos_l2_interfaces: + config: + - name: Ethernet1/2 + access: + vlan: 30 + state: overridden + +# After state: +# ------------ +# +# interface Ethernet1/1 +# interface Ethernet1/2 +# switchport access vlan 30 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + + +# Using deleted + +# Before state: +# ------------- +# +# interface Ethernet1/1 +# switchport access vlan 20 +# interface Ethernet1/2 +# switchport trunk native vlan 20 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + +- name: Delete L2 attributes of given interfaces (Note This won't delete the interface + itself). + cisco.nxos.nxos_l2_interfaces: + config: + - name: Ethernet1/1 + - name: Ethernet1/2 + state: deleted + +# After state: +# ------------ +# +# interface Ethernet1/1 +# interface Ethernet1/2 +# interface mgmt0 +# ip address dhcp +# ipv6 address auto-config + +# Using rendered + +- name: Render platform specific configuration lines (without connecting to the device) + cisco.nxos.nxos_l2_interfaces: + config: + - name: Ethernet1/1 + trunk: + native_vlan: 10 + allowed_vlans: 2,4,15 + - name: Ethernet1/2 + access: + vlan: 30 + - name: Ethernet1/3 + trunk: + native_vlan: 20 + allowed_vlans: 5-10, 15 + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/1" +# - "switchport trunk allowed vlan 2,4,15" +# - "switchport trunk native vlan 10" +# - "interface Ethernet1/2" +# - "switchport access vlan 30" +# - "interface Ethernet1/3" +# - "switchport trunk allowed vlan 5,6,7,8,9,10,15" +# - "switchport trunk native vlan 20" + +# Using parsed + +# parsed.cfg +# ------------ +# interface Ethernet1/800 +# switchport access vlan 18 +# switchport trunk allowed vlan 210 +# interface Ethernet1/801 +# switchport trunk allowed vlan 2,4,15 + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_l2_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# - name: Ethernet1/800 +# access: +# vlan: 18 +# trunk: +# allowed_vlans: "210" +# - name: Ethernet1/801 +# trunk: +# allowed_vlans: "2,4,15" + +# Using gathered + +# Existing device config state +# ------------------------------- +# Nexus9kvI5# sh running-config | section ^interface +# interface Ethernet1/1 +# switchport access vlan 6 +# switchport trunk allowed vlan 200 +# interface Ethernet1/2 +# switchport trunk native vlan 10 + +- name: Gather l2_interfaces facts from the device using nxos_l2_interfaces + cisco.nxos.nxos_l2_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# - name: "Ethernet1/1" +# access: +# vlan: 6 +# trunk: +# allowed_vlans: "200" +# +# - name: "Ethernet1/2" +# trunk: +# native_vlan: 10 +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - "interface Ethernet1/1" + - "switchport trunk allowed vlan 2,4,15" + - "switchport trunk native vlan 10" + - "interface Ethernet1/2" + - "switchport access vlan 30" + - "interface Ethernet1/3" + - "switchport trunk allowed vlan 5,6,7,8,9,10,15" + - "switchport trunk native vlan 20" +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.l2_interfaces.l2_interfaces import ( + L2_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.l2_interfaces.l2_interfaces import ( + L2_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=L2_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = L2_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_l3_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_l3_interfaces.py new file mode 100644 index 00000000..7b5cb7a6 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_l3_interfaces.py @@ -0,0 +1,411 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_l3_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_l3_interfaces +short_description: L3 interfaces resource module +description: This module manages Layer-3 interfaces attributes of NX-OS Interfaces. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^interface'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of Layer-3 interface options + type: list + elements: dict + suboptions: + name: + description: + - Full name of L3 interface, i.e. Ethernet1/1. + type: str + required: true + dot1q: + description: + - Configures IEEE 802.1Q VLAN encapsulation on a subinterface. + type: int + ipv4: + description: + - IPv4 address and attributes of the L3 interface. + type: list + elements: dict + suboptions: + address: + description: + - IPV4 address of the L3 interface. + type: str + tag: + description: + - URIB route tag value for local/direct routes. + type: int + secondary: + description: + - A boolean attribute to manage addition of secondary IP address. + type: bool + ipv6: + description: + - IPv6 address and attributes of the L3 interface. + type: list + elements: dict + suboptions: + address: + description: + - IPV6 address of the L3 interface. + type: str + tag: + description: + - URIB route tag value for local/direct routes. + type: int + redirects: + description: + - Enables/disables ipv4 redirects. + type: bool + ipv6_redirects: + description: + - Enables/disables ipv6 redirects. + type: bool + unreachables: + description: + - Enables/disables ip redirects + type: bool + evpn_multisite_tracking: + description: + - VxLAN evpn multisite Interface tracking. Supported only on selected model. + type: str + version_added: 1.1.0 + choices: + - fabric-tracking + - dci-tracking + state: + description: + - The state of the configuration after module completion. + - The state I(overridden) would override the IP address configuration + of all interfaces on the device with the provided configuration in + the task. Use caution with this state as you may loose access to the + device. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# +# interface Ethernet1/6 + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_l3_interfaces: + config: + - name: Ethernet1/6 + ipv4: + - address: 192.168.1.1/24 + tag: 5 + - address: 10.1.1.1/24 + secondary: true + tag: 10 + ipv6: + - address: fd5d:12c9:2201:2::1/64 + tag: 6 + - name: Ethernet1/7.42 + dot1q: 42 + redirects: false + unreachables: false + state: merged + +# After state: +# ------------ +# +# interface Ethernet1/6 +# ip address 192.168.22.1/24 tag 5 +# ip address 10.1.1.1/24 secondary tag 10 +# interface Ethernet1/6 +# ipv6 address fd5d:12c9:2201:2::1/64 tag 6 +# interface Ethernet1/7.42 +# encapsulation dot1q 42 +# no ip redirects +# no ip unreachables + + +# Using replaced + +# Before state: +# ------------- +# +# interface Ethernet1/6 +# ip address 192.168.22.1/24 +# ipv6 address "fd5d:12c9:2201:1::1/64" + +- name: Replace device configuration of specified L3 interfaces with provided configuration. + cisco.nxos.nxos_l3_interfaces: + config: + - name: Ethernet1/6 + ipv4: + - address: 192.168.22.3/24 + state: replaced + +# After state: +# ------------ +# +# interface Ethernet1/6 +# ip address 192.168.22.3/24 + + +# Using overridden + +# Before state: +# ------------- +# +# interface Ethernet1/2 +# ip address 192.168.22.1/24 +# interface Ethernet1/6 +# ipv6 address "fd5d:12c9:2201:1::1/64" + +- name: Override device configuration of all L3 interfaces on device with provided + configuration. + cisco.nxos.nxos_l3_interfaces: + config: + - name: Ethernet1/2 + ipv4: 192.168.22.3/4 + state: overridden + +# After state: +# ------------ +# +# interface Ethernet1/2 +# ipv4 address 192.168.22.3/24 +# interface Ethernet1/6 + + +# Using deleted + +# Before state: +# ------------- +# +# interface Ethernet1/6 +# ip address 192.168.22.1/24 +# interface Ethernet1/2 +# ipv6 address "fd5d:12c9:2201:1::1/64" + +- name: Delete L3 attributes of given interfaces (This won't delete the interface + itself). + cisco.nxos.nxos_l3_interfaces: + config: + - name: Ethernet1/6 + - name: Ethernet1/2 + state: deleted + +# After state: +# ------------ +# +# interface Ethernet1/6 +# interface Ethernet1/2 + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_l3_interfaces: + config: + - name: Ethernet1/800 + ipv4: + - address: 192.168.1.100/24 + tag: 5 + - address: 10.1.1.1/24 + secondary: true + tag: 10 + - name: Ethernet1/800 + ipv6: + - address: fd5d:12c9:2201:2::1/64 + tag: 6 + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/800" +# - "ip address 192.168.1.100/24 tag 5" +# - "ip address 10.1.1.1/24 secondary tag 10" +# - "interface Ethernet1/800" +# - "ipv6 address fd5d:12c9:2201:2::1/64 tag 6" + +# Using parsed + +# parsed.cfg +# ------------ +# interface Ethernet1/800 +# ip address 192.168.1.100/24 tag 5 +# ip address 10.1.1.1/24 secondary tag 10 +# no ip redirects +# interface Ethernet1/801 +# ipv6 address fd5d:12c9:2201:2::1/64 tag 6 +# ip unreachables +# interface mgmt0 +# ip address dhcp +# vrf member management + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_l3_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - name: Ethernet1/800 +# ipv4: +# - address: 192.168.1.100/24 +# tag: 5 +# - address: 10.1.1.1/24 +# secondary: True +# tag: 10 +# redirects: False +# - name: Ethernet1/801 +# ipv6: +# - address: fd5d:12c9:2201:2::1/64 +# tag: 6 +# unreachables: True + +# Using gathered + +# Existing device config state +# ------------------------------- +# interface Ethernet1/1 +# ip address 192.0.2.100/24 +# interface Ethernet1/2 +# no ip redirects +# ip address 203.0.113.10/24 +# ip unreachables +# ipv6 address 2001:db8::1/32 + +- name: Gather l3_interfaces facts from the device using nxos_l3_interfaces + cisco.nxos.nxos_l3_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- + +# gathered: +# - name: Ethernet1/1 +# ipv4: +# - address: 192.0.2.100/24 +# - name: Ethernet1/2 +# ipv4: +# - address: 203.0.113.10/24 +# ipv6: +# - address: 2001:db8::1/32 +# redirects: False +# unreachables: True +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/2', 'ip address 192.168.0.1/2'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.l3_interfaces.l3_interfaces import ( + L3_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.l3_interfaces.l3_interfaces import ( + L3_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=L3_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = L3_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp.py new file mode 100644 index 00000000..23349113 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp.py @@ -0,0 +1,276 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_lacp +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_lacp +short_description: LACP resource module +description: This module manages Global Link Aggregation Control Protocol (LACP) on + NX-OS devices. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL. +- Unsupported for Cisco MDS +- Feature lacp should be enabled for this module. +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | include lacp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: LACP global options. + type: dict + suboptions: + system: + description: + - LACP system options + type: dict + suboptions: + priority: + description: + - The system priority to use in LACP negotiations. + type: int + mac: + description: + - MAC address to be used for the LACP Protocol exchanges + type: dict + suboptions: + address: + description: + - MAC-address (FORMAT :xxxx.xxxx.xxxx). + type: str + role: + description: + - The role for the Switch. + type: str + choices: + - primary + - secondary + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_lacp: + config: + system: + priority: 10 + mac: + address: 00c1.4c00.bd15 + state: merged + +# After state: +# ------------ +# +# lacp system-priority 10 +# lacp system-mac 00c1.4c00.bd15 + + +# Using replaced + +# Before state: +# ------------- +# +# lacp system-priority 10 + +- name: Replace device global lacp configuration with the given configuration. + cisco.nxos.nxos_lacp: + config: + system: + mac: + address: 00c1.4c00.bd15 + state: replaced + +# After state: +# ------------ +# +# lacp system-mac 00c1.4c00.bd15 + + +# Using deleted + +# Before state: +# ------------- +# +# lacp system-priority 10 + +- name: Delete global LACP configurations. + cisco.nxos.nxos_lacp: + state: deleted + +# After state: +# ------------ +# + +# Using rendered + +- name: Render platform specific configuration lines (without connecting to the device) + cisco.nxos.nxos_lacp: + config: + system: + priority: 10 + mac: + address: 00c1.4c00.bd15 + role: secondary + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "lacp system-priority 10" +# - "lacp system-mac 00c1.4c00.bd15 role secondary" + +# Using parsed + +# parsed.cfg +# ------------ +# lacp system-priority 10 +# lacp system-mac 00c1.4c00.bd15 role secondary + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_lacp: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# system: +# priority: 10 +# mac: +# address: 00c1.4c00.bd15 +# role: secondary + +# Using gathered + +# Existing device config state +# ------------------------------- +# Nexus9000v# show running-config | include lacp +# lacp system-priority 11 +# lacp system-mac 00c1.4c00.bd15 role primary + +- name: Gather lacp facts from the device using nxos_lacp + cisco.nxos.nxos_lacp: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# system: +# priority: 11 +# mac: +# address: 00c1.4c00.bd15 +# role: primary +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['lacp system-priority 15', 'lacp system-mac 00c1.4c00.bd15 role primary'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.lacp.lacp import ( + LacpArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.lacp.lacp import Lacp + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=LacpArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Lacp(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp_interfaces.py new file mode 100644 index 00000000..98e5a633 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_lacp_interfaces.py @@ -0,0 +1,381 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_lacp_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_lacp_interfaces +short_description: LACP interfaces resource module +description: This module manages Link Aggregation Control Protocol (LACP) attributes + of NX-OS Interfaces. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of LACP interfaces options. + type: list + elements: dict + suboptions: + name: + description: + - Name of the interface. + required: true + type: str + port_priority: + description: + - LACP port priority for the interface. Range 1-65535. Applicable only for + Ethernet. + type: int + rate: + description: + - Rate at which PDUs are sent by LACP. Applicable only for Ethernet. At fast + rate LACP is transmitted once every 1 second. At normal rate LACP is transmitted + every 30 seconds after the link is bundled. + type: str + choices: + - fast + - normal + links: + description: + - This dict contains configurable options related to max and min port-channel + links. Applicable only for Port-channel. + type: dict + suboptions: + max: + description: + - Port-channel max bundle. + type: int + min: + description: + - Port-channel min links. + type: int + mode: + description: + - LACP mode. Applicable only for Port-channel. + type: str + choices: + - delay + suspend_individual: + description: + - port-channel lacp state. Disabling this will cause lacp to put the port + to individual state and not suspend the port in case it does not get LACP + BPDU from the peer ports in the port-channel. + type: bool + convergence: + description: + - This dict contains configurable options related to convergence. Applicable + only for Port-channel. + type: dict + suboptions: + graceful: + description: + - port-channel lacp graceful convergence. Disable this only with lacp + ports connected to Non-Nexus peer. Disabling this with Nexus peer can + lead to port suspension. + type: bool + vpc: + description: + - Enable lacp convergence for vPC port channels. + type: bool + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_lacp_interfaces: + config: + - name: Ethernet1/3 + port_priority: 5 + rate: fast + state: merged + +# After state: +# ------------ +# +# interface Ethernet1/3 +# lacp port-priority 5 +# lacp rate fast + + +# Using replaced + +# Before state: +# ------------- +# +# interface Ethernet1/3 +# lacp port-priority 5 +# interface port-channel11 +# lacp mode delay + +- name: Replace device lacp interfaces configuration with the given configuration. + cisco.nxos.nxos_lacp_interfaces: + config: + - name: port-channel11 + links: + min: 4 + state: replaced + +# After state: +# ------------ +# +# interface Ethernet1/3 +# lacp port-priority 5 +# interface port-channel11 +# lacp min-links 4 + + +# Using overridden + +# Before state: +# ------------- +# +# interface Ethernet1/3 +# lacp port-priority 5 +# interface port-channel11 +# lacp mode delay + +- name: Override device configuration of all LACP interfaces attributes of given interfaces + on device with provided configuration. + cisco.nxos.nxos_lacp_interfaces: + config: + - name: port-channel11 + links: + min: 4 + state: overridden + +# After state: +# ------------ +# +# interface port-channel11 +# lacp min-links 4 + + +# Using deleted + +# Before state: +# ------------- +# +# interface Ethernet1/3 +# lacp port-priority 5 +# interface port-channel11 +# lacp mode delay + +- name: Delete LACP interfaces configurations. + cisco.nxos.nxos_lacp_interfaces: + state: deleted + +# After state: +# ------------ +# + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_lacp_interfaces: + config: + - name: Ethernet1/800 + rate: fast + - name: Ethernet1/801 + rate: fast + port_priority: 32 + - name: port-channel10 + links: + max: 15 + min: 2 + convergence: + graceful: true + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/800" +# - "lacp rate fast" +# - "interface Ethernet1/801" +# - "lacp port-priority 32" +# - "lacp rate fast" +# - "interface port-channel10" +# - "lacp min-links 2" +# - "lacp max-bundle 15" +# - "lacp graceful-convergence" + +# Using parsed + +# parsed.cfg +# ------------ + +# interface port-channel10 +# lacp min-links 10 +# lacp max-bundle 15 +# interface Ethernet1/800 +# lacp port-priority 100 +# lacp rate fast + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_lacp_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - name: port-channel10 +# links: +# max: 15 +# min: 10 +# - name: Ethernet1/800 +# port_priority: 100 +# rate: fast + +# Using gathered + +# Existing device config state +# ------------------------------- +# interface Ethernet1/1 +# lacp port-priority 5 +# lacp rate fast +# interface port-channel10 +# lacp mode delay +# interface port-channel11 +# lacp max-bundle 10 +# lacp min-links 5 + +- name: Gather lacp_interfaces facts from the device using nxos_lacp_interfaces + cisco.nxos.nxos_lacp_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# - name: Ethernet1/1 +# port_priority: 5 +# rate: fast +# - name: port-channel10 +# mode: delay +# - name: port-channel11 +# links: +# max: 10 +# min: 5 +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface port-channel10', 'lacp min-links 5', 'lacp mode delay'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.lacp_interfaces.lacp_interfaces import ( + Lacp_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.lacp_interfaces.lacp_interfaces import ( + Lacp_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Lacp_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Lacp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_lag_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_lag_interfaces.py new file mode 100644 index 00000000..ce06462e --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_lag_interfaces.py @@ -0,0 +1,368 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_lag_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_lag_interfaces +short_description: LAG interfaces resource module +description: This module manages attributes of link aggregation groups of NX-OS Interfaces. +version_added: 1.0.0 +author: +- Trishna Guha (@trishnaguha) +- Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of link aggregation group configurations. + type: list + elements: dict + suboptions: + name: + description: + - Name of the link aggregation group (LAG). + type: str + required: true + members: + description: + - The list of interfaces that are part of the group. + type: list + elements: dict + suboptions: + member: + description: + - The interface name. + type: str + mode: + description: + - Link aggregation group (LAG). + type: str + choices: + - 'active' + - 'on' + - 'passive' + force: + description: + - When true it forces link aggregation group members to match what is + declared in the members param. This can be used to remove members. + type: bool + state: + description: + - The state of the configuration after module completion. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL. +- Unsupported for Cisco MDS +- This module works with connection C(network_cli). + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# +# interface Ethernet1/4 + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_lag_interfaces: + config: + - name: port-channel99 + members: + - member: Ethernet1/4 + state: merged + +# After state: +# ------------ +# +# interface Ethernet1/4 +# channel-group 99 + + +# Using replaced + +# Before state: +# ------------- +# +# interface Ethernet1/4 +# channel-group 99 mode active + +- name: Replace device configuration of specified LAG attributes of given interfaces + with provided configuration. + cisco.nxos.nxos_lag_interfaces: + config: + - name: port-channel10 + members: + - member: Ethernet1/4 + state: replaced + +# After state: +# ------------ +# +# interface Ethernet1/4 +# channel-group 10 + + +# Using overridden + +# Before state: +# ------------- +# +# interface Ethernet1/4 +# channel-group 10 +# interface Ethernet1/2 +# channel-group 99 mode passive + +- name: Override device configuration of all LAG attributes of given interfaces on + device with provided configuration. + cisco.nxos.nxos_lag_interfaces: + config: + - name: port-channel20 + members: + - member: Ethernet1/6 + force: true + state: overridden + +# After state: +# ------------ +# interface Ethernet1/2 +# interface Ethernet1/4 +# interface Ethernet1/6 +# channel-group 20 force + + +# Using deleted + +# Before state: +# ------------- +# +# interface Ethernet1/4 +# channel-group 99 mode active + +- name: Delete LAG attributes of given interface (This won't delete the port-channel + itself). + cisco.nxos.nxos_lag_interfaces: + config: + - port-channel: port-channel99 + state: deleted + +- name: Delete LAG attributes of all the interfaces + cisco.nxos.nxos_lag_interfaces: + state: deleted + +# After state: +# ------------ +# +# interface Ethernet1/4 +# no channel-group 99 + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_lag_interfaces: + config: + - name: port-channel10 + members: + - member: Ethernet1/800 + mode: active + - member: Ethernet1/801 + - name: port-channel11 + members: + - member: Ethernet1/802 + mode: passive + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "interface Ethernet1/800" +# - "channel-group 10 mode active" +# - "interface Ethernet1/801" +# - "channel-group 10" +# - "interface Ethernet1/802" +# - "channel-group 11 mode passive" + +# Using parsed + +# parsed.cfg +# ------------ + +# interface port-channel10 +# interface port-channel11 +# interface port-channel12 +# interface Ethernet1/800 +# channel-group 10 mode active +# interface Ethernet1/801 +# channel-group 10 mode active +# interface Ethernet1/802 +# channel-group 11 mode passive +# interface Ethernet1/803 +# channel-group 11 mode passive + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_lag_interfaces: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - members: +# - member: Ethernet1/800 +# mode: active +# - member: Ethernet1/801 +# mode: active +# name: port-channel10 +# +# - members: +# - member: Ethernet1/802 +# mode: passive +# - member: Ethernet1/803 +# mode: passive +# name: port-channel11 +# +# - name: port-channel12 + +# Using gathered + +# Existing device config state +# ------------------------------- +# interface port-channel10 +# interface port-channel11 +# interface Ethernet1/1 +# channel-group 10 mode active +# interface Ethernet1/2 +# channel-group 11 mode passive +# + +- name: Gather lag_interfaces facts from the device using nxos_lag_interfaces + cisco.nxos.nxos_lag_interfaces: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# - name: port-channel10 +# members: +# - member: Ethernet1/1 +# mode: active +# - name: port-channel11 +# members: +# - member: Ethernet1/2 +# mode: passive +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - "interface Ethernet1/800" + - "channel-group 10 mode active" + - "interface Ethernet1/801" + - "channel-group 10" + - "interface Ethernet1/802" + - "channel-group 11 mode passive" +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.lag_interfaces.lag_interfaces import ( + Lag_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.lag_interfaces.lag_interfaces import ( + Lag_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Lag_interfacesArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Lag_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_global.py new file mode 100644 index 00000000..3bd3c553 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_global.py @@ -0,0 +1,345 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The module file for nxos_lldp_global +""" +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_lldp_global +short_description: LLDP resource module +description: This module configures and manages the Link Layer Discovery Protocol(LLDP) + attributes on NX-OS platforms. +version_added: 1.0.0 +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: +- Tested against NxOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- The LLDP feature needs to be enabled before using this module +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | include lldp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: + - A list of link layer discovery configurations + type: dict + suboptions: + holdtime: + description: + - Amount of time the receiving device should hold the information (in seconds) + type: int + port_id: + description: + - This attribute defines if the interface names should be advertised in the + long(0) or short(1) form. + type: int + choices: + - 0 + - 1 + reinit: + description: + - Amount of time to delay the initialization of LLDP on any interface (in + seconds) + type: int + timer: + description: + - Frequency at which LLDP updates need to be transmitted (in seconds) + type: int + tlv_select: + description: + - This attribute can be used to specify the TLVs that need to be sent and + received in the LLDP packets. By default, all TLVs are advertised + type: dict + suboptions: + dcbxp: + description: + - Used to specify the Data Center Bridging Exchange Protocol TLV + type: bool + management_address: + description: + - Used to specify the management address in TLV messages + type: dict + suboptions: + v4: + description: Management address with TLV v4 + type: bool + v6: + description: Management address with TLV v6 + type: bool + port: + description: + - Used to manage port based attributes in TLV messages + type: dict + suboptions: + description: + description: + - Used to specify the port description TLV + type: bool + vlan: + description: + - Used to specify the port VLAN ID TLV + type: bool + power_management: + description: + - Used to specify IEEE 802.3 DTE Power via MDI TLV + type: bool + system: + description: + - Used to manage system based attributes in TLV messages + type: dict + suboptions: + capabilities: + description: + - Used to specify the system capabilities TLV + type: bool + description: + description: + - Used to specify the system description TLV + type: bool + name: + description: + - Used to specify the system name TLV + type: bool + state: + description: + - The state of the configuration after module completion + type: str + choices: + - merged + - replaced + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged +# Before state: +# ------------- +# +# user(config)# show running-config | include lldp +# feature lldp + +- name: Merge provided configuration with device configuration + cisco.nxos.nxos_lldp_global: + config: + timer: 35 + holdtime: 100 + state: merged + +# After state: +# ------------ +# +# user(config)# show running-config | include lldp +# feature lldp +# lldp timer 35 +# lldp holdtime 100 + + +# Using replaced +# Before state: +# ------------- +# +# user(config)# show running-config | include lldp +# feature lldp +# lldp holdtime 100 +# lldp reinit 5 +# lldp timer 35 + +- name: Replace device configuration of specific LLDP attributes with provided configuration + cisco.nxos.nxos_lldp_global: + config: + timer: 40 + tlv_select: + system: + description: true + name: false + management_address: + v4: true + state: replaced + +# After state: +# ------------ +# +# user(config)# show running-config | include lldp +# feature lldp +# lldp timer 40 +# no lldp tlv-select system-name + + +# Using deleted +# Before state: +# ------------- +# +# user(config)# show running-config | include lldp +# feature lldp +# lldp holdtime 5 +# lldp reinit 3 + +- name: Delete LLDP configuration (this will by default remove all lldp configuration) + cisco.nxos.nxos_lldp_global: + state: deleted + +# After state: +# ------------ +# +# user(config)# show running-config | include lldp +# feature lldp + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_lldp_global: + config: + holdtime: 130 + port_id: 1 + reinit: 5 + tlv_select: + dcbxp: yes + power_management: yes + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - "lldp tlv-select dcbxp" +# - "lldp tlv-select power-management" +# - "lldp portid-subtype 1" +# - "lldp reinit 5" +# - "lldp holdtime 130" + +# Using parsed + +# parsed.cfg +# ------------ +# lldp holdtime 131 +# lldp reinit 7 +# no lldp tlv-select system-name +# no lldp tlv-select system-description + +# Task output (redacted) +# ----------------------- + +# parsed: +# holdtime: 131 +# reinit: 7 +# tlv_select: +# system: +# description: false +# name: false + +# Using gathered + +# Existing device config state +# ------------------------------- +# feature lldp +# lldp holdtime 129 +# lldp reinit 5 +# lldp timer 35 +# no lldp tlv-select system-name + +# Task output (redacted) +# ----------------------- + +# gathered: +# reinit: 5 +# timer: 35 +# tlv_select: +# system: +# name: False +# holdtime: 129 +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['lldp holdtime 125', 'lldp reinit 4', 'no lldp tlv-select system-name'] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.lldp_global.lldp_global import ( + Lldp_globalArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.lldp_global.lldp_global import ( + Lldp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + mutually_exclusive = [("config", "running_config")] + + module = AnsibleModule( + argument_spec=Lldp_globalArgs.argument_spec, + required_if=required_if, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = Lldp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_interfaces.py new file mode 100644 index 00000000..cf49a9ac --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_lldp_interfaces.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The module file for nxos_lldp_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_lldp_interfaces +short_description: LLDP interfaces resource module +description: This module manages interfaces' configuration for Link Layer Discovery + Protocol (LLDP) on NX-OS platforms. +version_added: 1.0.0 +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- The LLDP feature needs to be enabled before using this module +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section ^interface). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: + - A list of link layer discovery configurations for interfaces. + type: list + elements: dict + suboptions: + name: + description: + - Name of the interface + required: true + type: str + receive: + description: + - Used to enable or disable the reception of LLDP packets on that interface. + By default, this is enabled after LLDP is enabled globally. + type: bool + transmit: + description: + - Used to enable or disable the transmission of LLDP packets on that interface. + By default, this is enabled after LLDP is enabled globally. + type: bool + tlv_set: + description: + - Used to configure TLV parameters on the interface + type: dict + suboptions: + management_address: + description: + - Used to mention the IPv4 or IPv6 management address for the interface + type: str + vlan: + description: + - Used to mention the VLAN for the interface + type: int + state: + description: + - The state the configuration should be left in + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# + +- name: Merge provided configuration with device configuration + cisco.nxos.nxos_lldp_interfaces: + config: + - name: Ethernet1/4 + receive: false + transmit: true + tlv_set: + management_address: 192.168.122.64 + vlan: 12 + state: merged + +# After state: +# ------------- +# +# interface Ethernet1/4 +# no lldp receive +# lldp tlv-set management-address 192.168.122.64 +# lldp tlv-set vlan 12 + + +# Using replaced + +# Before state: +# ------------ +# +# interface Ethernet1/4 +# no lldp receive +# lldp tlv-set management-address 192.168.122.64 +# interface Ethernet1/5 +# no lldp transmit +# lldp tlv-set vlan 10 + +- name: Replace LLDP configuration on interfaces with given configuration + cisco.nxos.nxos_lldp_interfaces: + config: + - name: Ethernet1/4 + transmit: no + tlv_set: + vlan: 2 + state: replaced + + +# After state: +# ----------- +# +# interface Ethernet1/4 +# no lldp transmit +# lldp tlv_set vlan 2 +# interface Ethernet1/5 +# no lldp transmit +# lldp tlv-set vlan 10 + + +# Using overridden + +# Before state: +# ------------ +# +# interface Ethernet1/4 +# no lldp receive +# lldp tlv-set management-address 192.168.122.64 +# interface Ethernet1/5 +# no lldp transmit +# lldp tlv-set vlan 10 + +- name: Override LLDP configuration on all interfaces with given configuration + cisco.nxos.nxos_lldp_interfaces: + config: + - name: Ethernet1/7 + receive: no + tlv_set: + vlan: 12 + state: overridden + + +# After state: +# ----------- +# +# interface Ethernet1/7 +# no lldp receive +# lldp tlv_set vlan 12 + + +# Using deleted + +# Before state: +# ------------ +# +# interface Ethernet1/4 +# lldp tlv-set management vlan 24 +# no lldp transmit +# interface mgmt0 +# no lldp receive + +- name: Delete LLDP interfaces configuration + cisco.nxos.nxos_lldp_interfaces: + state: deleted + +# After state: +# ------------ +# + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['interface Ethernet1/2', 'lldp receive', 'lldp tlv-set vlan 12'] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.lldp_interfaces.lldp_interfaces import ( + Lldp_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.lldp_interfaces.lldp_interfaces import ( + Lldp_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Lldp_interfacesArgs.argument_spec, + supports_check_mode=True, + ) + + result = Lldp_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_logging.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_logging.py new file mode 100644 index 00000000..7782eb32 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_logging.py @@ -0,0 +1,940 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# Copyright: (c) 2017, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = """ +module: nxos_logging +author: Trishna Guha (@trishnaguha) +short_description: Manage logging on network devices +notes: +- Limited Support for Cisco MDS +description: +- This module provides declarative management of logging on Cisco NX-OS devices. +version_added: 1.0.0 +deprecated: + alternative: nxos_logging_global + why: Updated module released with more functionality. + removed_at_date: '2023-08-01' +options: + dest: + description: + - Destination of the logs. + choices: + - console + - logfile + - module + - monitor + - server + type: str + remote_server: + description: + - Hostname or IP Address for remote logging (when dest is 'server'). + type: str + use_vrf: + description: + - VRF to be used while configuring remote logging (when dest is 'server'). + type: str + interface: + description: + - Interface to be used while configuring source-interface for logging (e.g., 'Ethernet1/2', + 'mgmt0') + type: str + name: + description: + - If value of C(dest) is I(logfile) it indicates file-name. + type: str + facility: + description: + - Facility name for logging. + type: str + dest_level: + description: + - Set logging severity levels. + aliases: + - level + type: int + facility_level: + description: + - Set logging severity levels for facility based log messages. + type: int + aggregate: + description: List of logging definitions. + type: list + elements: dict + state: + description: + - State of the logging configuration. + default: present + choices: + - present + - absent + type: str + event: + description: + - Link/trunk enable/default interface configuration logging + choices: + - link-enable + - link-default + - trunk-enable + - trunk-default + type: str + interface_message: + description: + - Add interface description to interface syslogs. Does not work with version 6.0 + images using nxapi as a transport. + choices: + - add-interface-description + type: str + file_size: + description: + - Set logfile size + type: int + facility_link_status: + description: + - Set logging facility ethpm link status. Not idempotent with version 6.0 images. + choices: + - link-down-notif + - link-down-error + - link-up-notif + - link-up-error + type: str + timestamp: + description: + - Set logging timestamp format + choices: + - microseconds + - milliseconds + - seconds + type: str + purge: + description: + - Remove any switch logging configuration that does not match what has been configured + Not supported for ansible_connection local. All nxos_logging tasks must use + the same ansible_connection type. + type: bool + default: false +extends_documentation_fragment: +- cisco.nxos.nxos +""" + +EXAMPLES = """ +- name: configure console logging with level + cisco.nxos.nxos_logging: + dest: console + level: 2 + state: present +- name: remove console logging configuration + cisco.nxos.nxos_logging: + dest: console + level: 2 + state: absent +- name: configure file logging with level + cisco.nxos.nxos_logging: + dest: logfile + name: testfile + dest_level: 3 + state: present +- name: Configure logging logfile with size + cisco.nxos.nxos_logging: + dest: logfile + name: testfile + dest_level: 3 + file_size: 16384 +- name: configure facility level logging + cisco.nxos.nxos_logging: + facility: daemon + facility_level: 0 + state: present +- name: remove facility level logging + cisco.nxos.nxos_logging: + facility: daemon + facility_level: 0 + state: absent +- name: Configure Remote Logging + cisco.nxos.nxos_logging: + dest: server + remote_server: test-syslogserver.com + facility: auth + facility_level: 1 + use_vrf: management + state: present +- name: Configure Source Interface for Logging + cisco.nxos.nxos_logging: + interface: mgmt0 + state: present +- name: Purge nxos_logging configuration not managed by this playbook + cisco.nxos.nxos_logging: + purge: true +- name: Configure logging timestamp + cisco.nxos.nxos_logging: + timestamp: milliseconds + state: present +- name: Configure logging facility ethpm link status + cisco.nxos.nxos_logging: + facility: ethpm + facility_link_status: link-up-notif + state: present +- name: Configure logging message ethernet description + cisco.nxos.nxos_logging: + interface_message: add-interface-description + state: present +- name: Configure logging event link enable + cisco.nxos.nxos_logging: + event: link-enable + state: present +- name: Configure logging using aggregate + cisco.nxos.nxos_logging: + aggregate: + - {dest: console, dest_level: 2} + - {dest: logfile, dest_level: 2, name: testfile} + - {facility: daemon, facility_level: 0} + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - logging console 2 + - logging logfile testfile 3 + - logging level daemon 0 +""" + +import copy +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, + normalize_interface, + read_module_context, + run_commands, + save_module_context, +) + + +STATIC_CLI = { + "link-enable": "logging event link-status enable", + "link-default": "logging event link-status default", + "trunk-enable": "logging event trunk-status enable", + "trunk-default": "logging event trunk-status default", + "microseconds": "logging timestamp microseconds", + "milliseconds": "logging timestamp milliseconds", + "seconds": "logging timestamp seconds", + "link-up-error": "link-up error", + "link-up-notif": "link-up notif", + "link-down-error": "link-down error", + "link-down-notif": "link-down notif", + "add-interface-description": "logging message interface type ethernet description", +} + +DEFAULT_LOGGING_LEVEL = { + 0: [], + 1: [], + 2: ["pktmgr"], + 3: ["adjmgr", "arp", "icmpv6", "l2rib", "netstack"], + 4: [], + 5: ["mrib", "m6rib"], + 6: [], + 7: [], +} + +DEST_GROUP = ["console", "logfile", "module", "monitor", "server"] + + +def map_obj_to_commands(module, updates): + commands = list() + want, have = updates + + for w in want: + state = w["state"] + del w["state"] + + if state == "absent" and w in have: + if w["facility"] is not None: + if ( + not w["dest"] + and not w["facility_link_status"] + and w["facility"] not in DEFAULT_LOGGING_LEVEL[int(w["facility_level"])] + ): + commands.append( + "no logging level {0} {1}".format(w["facility"], w["facility_level"]), + ) + + if w["facility_link_status"] and w["facility"] in ("ethpm"): + commands.append( + "no logging level {0} {1}".format( + w["facility"], + STATIC_CLI[w["facility_link_status"]], + ), + ) + + if w["name"] is not None: + commands.append("no logging logfile") + + if w["dest"] in ("console", "module", "monitor"): + commands.append("no logging {0}".format(w["dest"])) + + if w["dest"] == "server": + commands.append("no logging server {0}".format(w["remote_server"])) + + if w["interface"]: + commands.append("no logging source-interface") + + if w["event"] and w["event"] in STATIC_CLI: + commands.append("no " + STATIC_CLI[w["event"]]) + + if w["message"] and w["message"] in STATIC_CLI: + commands.append("no " + STATIC_CLI[w["message"]]) + + if w["timestamp"] and w["timestamp"] in STATIC_CLI: + commands.append("no " + STATIC_CLI[w["timestamp"]]) + + if state == "present" and w not in have: + if w["facility"] is None: + if w["dest"]: + if w["dest"] not in ("logfile", "server"): + commands.append("logging {0} {1}".format(w["dest"], w["dest_level"])) + + elif w["dest"] == "logfile": + if w["file_size"]: + commands.append( + "logging logfile {0} {1} size {2}".format( + w["name"], + w["dest_level"], + w["file_size"], + ), + ) + else: + commands.append( + "logging logfile {0} {1}".format(w["name"], w["dest_level"]), + ) + + elif w["dest"] == "server": + if w["facility_level"]: + if w["use_vrf"]: + commands.append( + "logging server {0} {1} use-vrf {2}".format( + w["remote_server"], + w["facility_level"], + w["use_vrf"], + ), + ) + else: + commands.append( + "logging server {0} {1}".format( + w["remote_server"], + w["facility_level"], + ), + ) + + else: + if w["use_vrf"]: + commands.append( + "logging server {0} use-vrf {1}".format( + w["remote_server"], + w["use_vrf"], + ), + ) + else: + commands.append("logging server {0}".format(w["remote_server"])) + + if w["facility"]: + if w["dest"] == "server": + if w["facility_level"]: + if w["use_vrf"]: + commands.append( + "logging server {0} {1} facility {2} use-vrf {3}".format( + w["remote_server"], + w["facility_level"], + w["facility"], + w["use_vrf"], + ), + ) + else: + commands.append( + "logging server {0} {1} facility {2}".format( + w["remote_server"], + w["facility_level"], + w["facility"], + ), + ) + else: + if w["use_vrf"]: + commands.append( + "logging server {0} facility {1} use-vrf {2}".format( + w["remote_server"], + w["facility"], + w["use_vrf"], + ), + ) + else: + commands.append( + "logging server {0} facility {1}".format( + w["remote_server"], + w["facility"], + ), + ) + else: + if w["facility_link_status"]: + commands.append( + "logging level {0} {1}".format( + w["facility"], + STATIC_CLI[w["facility_link_status"]], + ), + ) + else: + if not match_facility_default(module, w["facility"], w["facility_level"]): + commands.append( + "logging level {0} {1}".format(w["facility"], w["facility_level"]), + ) + + if w["interface"]: + commands.append( + "logging source-interface {0} {1}".format(*split_interface(w["interface"])), + ) + + if w["event"] and w["event"] in STATIC_CLI: + commands.append(STATIC_CLI[w["event"]]) + + if w["message"] and w["message"] in STATIC_CLI: + commands.append(STATIC_CLI[w["message"]]) + + if w["timestamp"] and w["timestamp"] in STATIC_CLI: + commands.append(STATIC_CLI[w["timestamp"]]) + + return commands + + +def match_facility_default(module, facility, want_level): + """Check wanted facility to see if it matches current device default""" + + matches_default = False + # Sample output from show logging level command + # Facility Default Severity Current Session Severity + # -------- ---------------- ------------------------ + # bfd 5 5 + # + # 0(emergencies) 1(alerts) 2(critical) + # 3(errors) 4(warnings) 5(notifications) + # 6(information) 7(debugging) + + regexl = r"\S+\s+(\d+)\s+(\d+)" + cmd = { + "command": "show logging level {0}".format(facility), + "output": "text", + } + facility_data = run_commands(module, cmd) + for line in facility_data[0].split("\n"): + mo = re.search(regexl, line) + if mo and int(mo.group(1)) == int(want_level) and int(mo.group(2)) == int(want_level): + matches_default = True + + return matches_default + + +def split_interface(interface): + match = re.search(r"(\D+)(\S*)", interface, re.M) + if match: + return match.group(1), match.group(2) + + +def parse_facility_link_status(line, facility, status): + facility_link_status = None + + if facility is not None: + match = re.search(r"logging level {0} {1} (\S+)".format(facility, status), line, re.M) + if match: + facility_link_status = status + "-" + match.group(1) + + return facility_link_status + + +def parse_event_status(line, event): + status = None + + match = re.search(r"logging event {0} (\S+)".format(event + "-status"), line, re.M) + if match: + state = match.group(1) + if state: + status = state + + return status + + +def parse_event(line): + event = None + + match = re.search(r"logging event (\S+)", line, re.M) + if match: + state = match.group(1) + if state == "link-status": + event = "link" + elif state == "trunk-status": + event = "trunk" + + return event + + +def parse_message(line): + message = None + + match = re.search(r"logging message interface type ethernet description", line, re.M) + if match: + message = "add-interface-description" + + return message + + +def parse_file_size(line, name, level): + file_size = None + + match = re.search(r"logging logfile {0} {1} size (\S+)".format(name, level), line, re.M) + if match: + file_size = match.group(1) + if file_size == "8192" or file_size == "4194304": + file_size = None + + return file_size + + +def parse_timestamp(line): + timestamp = None + + match = re.search(r"logging timestamp (\S+)", line, re.M) + if match: + timestamp = match.group(1) + + return timestamp + + +def parse_name(line, dest): + name = None + + if dest is not None: + if dest == "logfile": + match = re.search(r"logging logfile (\S+)", line, re.M) + if match: + name = match.group(1) + else: + pass + + return name + + +def parse_remote_server(line, dest): + remote_server = None + + if dest and dest == "server": + match = re.search(r"logging server (\S+)", line, re.M) + if match: + remote_server = match.group(1) + + return remote_server + + +def parse_dest_level(line, dest, name): + dest_level = None + + def parse_match(match): + level = None + if match: + if int(match.group(1)) in range(0, 8): + level = match.group(1) + else: + pass + return level + + if dest and dest != "server": + if dest == "logfile": + match = re.search(r"logging logfile {0} (\S+)".format(name), line, re.M) + if match: + dest_level = parse_match(match) + + elif dest == "server": + match = re.search(r"logging server (?:\S+) (\d+)", line, re.M) + if match: + dest_level = parse_match(match) + else: + match = re.search(r"logging {0} (\S+)".format(dest), line, re.M) + if match: + dest_level = parse_match(match) + + return dest_level + + +def parse_facility_level(line, facility, dest): + facility_level = None + + if dest == "server": + match = re.search(r"logging server (?:\S+) (\d+)", line, re.M) + if match: + facility_level = match.group(1) + + elif facility is not None: + match = re.search(r"logging level {0} (\S+)".format(facility), line, re.M) + if match: + facility_level = match.group(1) + + return facility_level + + +def parse_facility(line): + facility = None + + match = re.search( + r"logging server (?:\S+) (?:\d+) (?:\S+) (?:\S+) (?:\S+) (\S+)", + line, + re.M, + ) + if match: + facility = match.group(1) + + return facility + + +def parse_use_vrf(line, dest): + use_vrf = None + + if dest and dest == "server": + match = re.search(r"logging server (?:\S+) (?:\d+) use-vrf (\S+)", line, re.M) + if match: + use_vrf = match.group(1) + + return use_vrf + + +def parse_interface(line): + interface = None + + match = re.search(r"logging source-interface (\S*)", line, re.M) + if match: + interface = match.group(1) + + return interface + + +def map_config_to_obj(module): + obj = [] + + data = get_config(module, flags=[" all | section logging"]) + + for line in data.split("\n"): + if re.search(r"no (\S+)", line, re.M): + state = "absent" + else: + state = "present" + + match = re.search(r"logging (\S+)", line, re.M) + if state == "present" and match: + event_status = None + name = None + dest_level = None + dest = None + facility = None + remote_server = None + facility_link_status = None + file_size = None + facility_level = None + + if match.group(1) in DEST_GROUP: + dest = match.group(1) + + name = parse_name(line, dest) + remote_server = parse_remote_server(line, dest) + dest_level = parse_dest_level(line, dest, name) + + if dest == "server": + facility = parse_facility(line) + + facility_level = parse_facility_level(line, facility, dest) + + if dest == "logfile": + file_size = parse_file_size(line, name, dest_level) + + elif match.group(1) == "level": + match_facility = re.search(r"logging level (\S+)", line, re.M) + facility = match_facility.group(1) + + level = parse_facility_level(line, facility, dest) + if level.isdigit(): + facility_level = level + else: + facility_link_status = parse_facility_link_status(line, facility, level) + + elif match.group(1) == "event" and state == "present": + event = parse_event(line) + if event: + status = parse_event_status(line, event) + if status: + event_status = event + "-" + status + else: + continue + + else: + pass + + obj.append( + { + "dest": dest, + "remote_server": remote_server, + "use_vrf": parse_use_vrf(line, dest), + "name": name, + "facility": facility, + "dest_level": dest_level, + "facility_level": facility_level, + "interface": parse_interface(line), + "facility_link_status": facility_link_status, + "event": event_status, + "file_size": file_size, + "message": parse_message(line), + "timestamp": parse_timestamp(line), + }, + ) + + cmd = [ + { + "command": "show logging | section enabled | section console", + "output": "text", + }, + { + "command": "show logging | section enabled | section monitor", + "output": "text", + }, + ] + + default_data = run_commands(module, cmd) + + for line in default_data: + flag = False + match = re.search( + r"Logging (\w+):(?:\s+) (?:\w+) (?:\W)Severity: (\w+)", + str(line), + re.M, + ) + if match: + if match.group(1) == "console" and match.group(2) == "critical": + dest_level = "2" + flag = True + elif match.group(1) == "monitor" and match.group(2) == "notifications": + dest_level = "5" + flag = True + if flag: + obj.append( + { + "dest": match.group(1), + "remote_server": None, + "name": None, + "facility": None, + "dest_level": dest_level, + "facility_level": None, + "use_vrf": None, + "interface": None, + "facility_link_status": None, + "event": None, + "file_size": None, + "message": None, + "timestamp": None, + }, + ) + + return obj + + +def map_params_to_obj(module): + obj = [] + + if "aggregate" in module.params and module.params["aggregate"]: + args = { + "dest": "", + "remote_server": "", + "use_vrf": "", + "name": "", + "facility": "", + "dest_level": "", + "facility_level": "", + "interface": "", + "facility_link_status": None, + "event": None, + "file_size": None, + "message": None, + "timestamp": None, + } + + for c in module.params["aggregate"]: + d = c.copy() + + for key in args: + if key not in d: + d[key] = None + + if d["dest_level"] is not None: + d["dest_level"] = str(d["dest_level"]) + + if d["facility_level"] is not None: + d["facility_level"] = str(d["facility_level"]) + + if d["interface"]: + d["interface"] = normalize_interface(d["interface"]) + + if "state" not in d: + d["state"] = module.params["state"] + + if d["file_size"]: + d["file_size"] = str(d["file_size"]) + + obj.append(d) + + else: + dest_level = None + facility_level = None + file_size = None + + if module.params["dest_level"] is not None: + dest_level = str(module.params["dest_level"]) + + if module.params["facility_level"] is not None: + facility_level = str(module.params["facility_level"]) + + if module.params["file_size"] is not None: + file_size = str(module.params["file_size"]) + + obj.append( + { + "dest": module.params["dest"], + "remote_server": module.params["remote_server"], + "use_vrf": module.params["use_vrf"], + "name": module.params["name"], + "facility": module.params["facility"], + "dest_level": dest_level, + "facility_level": facility_level, + "interface": normalize_interface(module.params["interface"]), + "state": module.params["state"], + "facility_link_status": module.params["facility_link_status"], + "event": module.params["event"], + "message": module.params["interface_message"], + "file_size": file_size, + "timestamp": module.params["timestamp"], + }, + ) + return obj + + +def merge_wants(wants, want): + if not wants: + wants = list() + + for w in want: + w = copy.copy(w) + state = w["state"] + del w["state"] + + if state == "absent": + if w in wants: + wants.remove(w) + elif w not in wants: + wants.append(w) + + return wants + + +def absent(h): + h["state"] = "absent" + return h + + +def outliers(haves, wants): + wants = list(wants) + return [absent(h) for h in haves if not (h in wants or wants.append(h))] + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + dest=dict(choices=DEST_GROUP), + name=dict(), + facility=dict(), + remote_server=dict(), + use_vrf=dict(), + dest_level=dict(type="int", aliases=["level"]), + facility_level=dict(type="int"), + interface=dict(), + facility_link_status=dict( + choices=[ + "link-down-notif", + "link-down-error", + "link-up-notif", + "link-up-error", + ], + ), + event=dict( + choices=[ + "link-enable", + "link-default", + "trunk-enable", + "trunk-default", + ], + ), + interface_message=dict(choices=["add-interface-description"]), + file_size=dict(type="int"), + timestamp=dict(choices=["microseconds", "milliseconds", "seconds"]), + state=dict(default="present", choices=["present", "absent"]), + aggregate=dict(type="list", elements="dict"), + purge=dict(default=False, type="bool"), + ) + + required_if = [ + ("dest", "logfile", ["name"]), + ("dest", "server", ["remote_server"]), + ] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + want = map_params_to_obj(module) + merged_wants = merge_wants(read_module_context(module), want) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(module, (want, have)) + result["commands"] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + save_module_context(module, merged_wants) + + if module.params.get("purge"): + pcommands = map_obj_to_commands(module, (outliers(have, merged_wants), have)) + if pcommands: + if not module.check_mode: + load_config(module, pcommands) + result["changed"] = True + result["commands"] += pcommands + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_logging_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_logging_global.py new file mode 100644 index 00000000..1c060b01 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_logging_global.py @@ -0,0 +1,735 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_logging_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_logging_global +short_description: Logging resource module. +description: +- This module manages logging configuration on devices running Cisco NX-OS. +version_added: 2.5.0 +notes: +- Tested against NX-OS 9.3.6 on Cisco Nexus Switches. +- Limited Support for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +- Tested against Cisco MDS NX-OS 9.2(2) with connection C(network_cli). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | include logging). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of logging configuration. + type: dict + suboptions: + console: + description: Set console logging parameters. + type: dict + suboptions: + state: + description: Enable or disable monitor logging. + type: str + choices: ["enabled", "disabled"] + severity: &sev + description: Set severity severity for console. + type: str + choices: + - emergency + - alert + - critical + - error + - warning + - notification + - informational + - debugging + event: + description: Interface events. + type: dict + suboptions: + link_status: + description: UPDOWN and CHANGE messages. + type: dict + suboptions: &event + enable: + description: To enable logging overriding port severity configuration. + type: bool + default: + description: Default logging configuration used by interfaces not explicitly configured. + type: bool + trunk_status: + description: TRUNK status messages. + type: dict + suboptions: *event + history: + description: Modifies severity severity or size for history table. + type: dict + suboptions: + severity: *sev + size: + description: Set history table size. + type: int + ip: + description: + - IP configuration. + - This option is unsupported on MDS switches. + type: dict + suboptions: + access_list: + description: Access-List. + type: dict + suboptions: + cache: + description: Set caching settings. + type: dict + suboptions: + entries: + description: Maximum number of log entries cached in software. + type: int + interval: + description: Log-update interval (in sec). + type: int + threshold: + description: Log-update threshold (number of hits) + type: int + detailed: + description: Detailed ACL information. + type: bool + include: + description: Include additional fields in syslogs. + type: dict + suboptions: + sgt: + description: Include source group tag info in syslogs. + type: bool + facilities: + description: Facility parameter for syslog messages. + type: list + elements: dict + suboptions: + facility: + description: Facility name. + type: str + severity: *sev + logfile: + description: Set file logging. + type: dict + suboptions: + state: + description: Enable or disable logfile. + type: str + choices: ["enabled", "disabled"] + name: + description: Logfile name. + type: str + severity: *sev + persistent_threshold: + description: + - Set persistent logging utilization alert threshold in percentage. + - This option is unsupported on MDS switches. + type: int + size: + description: Enter the logfile size in bytes. + type: int + module: + description: Set module(linecard) logging. + type: dict + suboptions: + state: + description: Enable or disable module logging. + type: str + choices: ["enabled", "disabled"] + severity: *sev + monitor: + description: Set terminal line(monitor) logging severity. + type: dict + suboptions: + state: + description: Enable or disable monitor logging. + type: str + choices: ["enabled", "disabled"] + severity: *sev + origin_id: + description: Enable origin information for Remote Syslog Server. + type: dict + suboptions: + hostname: + description: + - Use hostname as origin-id of logging messages. + - This option is mutually exclusive with I(ip) and I(string). + type: bool + ip: + description: + - Use ip address as origin-id of logging messages. + - This option is mutually exclusive with I(hostname) and I(string). + type: str + string: + description: + - Use text string as origin-id of logging messages. + - This option is mutually exclusive with I(hostname) and I(ip). + type: str + rate_limit: + description: Enable or disable rate limit for log messages. + type: str + choices: ["enabled", "disabled"] + rfc_strict: + description: + - Set RFC to which messages should compliant. + - Syslogs will be compliant to RFC 5424. + - This option is unsupported on MDS switches. + type: bool + hosts: + description: Enable forwarding to Remote Syslog Servers. + type: list + elements: dict + suboptions: + host: + description: Hostname/IPv4/IPv6 address of the Remote Syslog Server. + type: str + severity: *sev + facility: + description: Facility to use when forwarding to server. + type: str + port: + description: Destination Port when forwarding to remote server. + type: int + secure: + description: Enable secure connection to remote server. + type: dict + suboptions: + trustpoint: + description: Trustpoint configuration. + type: dict + suboptions: + client_identity: + description: + - Client Identity certificate for mutual authentication. + - Trustpoint to use for client certificate authentication. + type: str + use_vrf: + description: + - Display per-VRF information. + - This option is unsupported on MDS switches. + type: str + source_interface: + description: + - Enable Source-Interface for Remote Syslog Server. + - This option is unsupported on MDS switches. + type: str + timestamp: + description: Set logging timestamp granularity. + type: str + choices: ["microseconds", "milliseconds", "seconds"] + state: + description: + - The state the configuration should be left in. + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - Refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config | include logging +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_logging_global: + config: + console: + severity: error + monitor: + severity: warning + ip: + access_list: + cache: + entries: 16384 + interval: 200 + threshold: 5000 + facilities: + - facility: auth + severity: critical + - facility: ospfv3 + severity: alert + - facility: ftp + severity: informational + hosts: + - host: 203.0.113.100 + severity: alert + use_vrf: management + - host: 203.0.113.101 + severity: error + facility: local6 + use_vrf: default + origin_id: + hostname: True + +# Task output +# ------------- +# before: {} +# +# commands: +# - "logging console 3" +# - "logging monitor 4" +# - "logging ip access-list cache entries 16384" +# - "logging ip access-list cache interval 200" +# - "logging ip access-list cache threshold 5000" +# - "logging severity auth 2" +# - "logging severity ospfv3 1" +# - "logging severity ftp 6" +# - "logging server 203.0.113.100 1 use-vrf management" +# - "logging server 203.0.113.101 3 facility local6 use-vrf default" +# - "logging origin-id hostname" +# +# after: +# console: +# severity: error +# facilities: +# - facility: auth +# severity: critical +# - facility: ftp +# severity: informational +# - facility: ospfv3 +# severity: alert +# ip: +# access_list: +# cache: +# entries: 16384 +# interval: 200 +# threshold: 5000 +# monitor: +# severity: warning +# origin_id: +# hostname: true +# hosts: +# - severity: alert +# host: 203.0.113.100 +# use_vrf: management +# - facility: local6 +# severity: error +# host: 203.0.113.101 +# use_vrf: default + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | include logging +# logging console 3 +# logging monitor 4 +# logging ip access-list cache entries 16384 +# logging ip access-list cache interval 200 +# logging ip access-list cache threshold 5000 +# logging severity auth 2 +# logging severity ospfv3 1 +# logging severity ftp 6 +# logging origin-id hostname +# logging server 203.0.113.100 1 use-vrf management +# logging server 203.0.113.101 3 use-vrf default facility local6 + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | include logging +# logging console 3 +# logging monitor 4 +# logging ip access-list cache entries 16384 +# logging ip access-list cache interval 200 +# logging ip access-list cache threshold 5000 +# logging severity auth 2 +# logging severity ospfv3 1 +# logging severity ftp 6 +# logging origin-id hostname +# logging server 203.0.113.100 1 use-vrf management +# logging server 203.0.113.101 3 use-vrf default facility local6 + +- name: Replace logging configurations with provided config + cisco.nxos.nxos_logging_global: + config: + monitor: + severity: warning + ip: + access_list: + cache: + entries: 4096 + facilities: + - facility: auth + severity: critical + - facility: ospfv3 + severity: alert + - facility: ftp + severity: informational + hosts: + - host: 203.0.113.101 + severity: error + facility: local6 + use_vrf: default + - host: 198.51.100.101 + severity: alert + port: 6538 + use_vrf: management + origin_id: + ip: 192.0.2.100 + state: replaced + +# Task output +# ------------- +# before: +# console: +# severity: error +# facilities: +# - facility: auth +# severity: critical +# - facility: ftp +# severity: informational +# - facility: ospfv3 +# severity: alert +# ip: +# access_list: +# cache: +# entries: 16384 +# interval: 200 +# threshold: 5000 +# monitor: +# severity: warning +# origin_id: +# hostname: true +# hosts: +# - severity: alert +# host: 203.0.113.100 +# use_vrf: management +# - facility: local6 +# severity: error +# host: 203.0.113.101 +# use_vrf: default +# +# commands: +# - "logging console" +# - "logging ip access-list cache entries 4096" +# - "no logging ip access-list cache interval 200" +# - "no logging ip access-list cache threshold 5000" +# - "no logging origin-id hostname" +# - "logging origin-id ip 192.0.2.100" +# - "logging server 198.51.100.101 1 port 6538 use-vrf management" +# - "no logging server 203.0.113.100 1 use-vrf management" +# +# after: +# facilities: +# - facility: auth +# severity: critical +# - facility: ftp +# severity: informational +# - facility: ospfv3 +# severity: alert +# ip: +# access_list: +# cache: +# entries: 4096 +# monitor: +# severity: warning +# origin_id: +# ip: 192.0.2.100 +# hosts: +# - severity: alert +# port: 6538 +# host: 198.51.100.101 +# use_vrf: management +# - facility: local6 +# severity: error +# host: 203.0.113.101 +# use_vrf: default +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | include logging +# logging monitor 4 +# logging ip access-list cache entries 4096 +# logging severity auth 2 +# logging severity ospfv3 1 +# logging severity ftp 6 +# logging origin-id ip 192.0.2.100 +# logging server 203.0.113.101 3 use-vrf default facility local6 +# logging server 198.51.100.101 1 port 6538 use-vrf management + +# Using deleted to delete all logging configurations + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | include logging +# logging console 3 +# logging monitor 4 +# logging ip access-list cache entries 16384 +# logging ip access-list cache interval 200 +# logging ip access-list cache threshold 5000 +# logging severity auth 2 +# logging severity ospfv3 1 +# logging severity ftp 6 +# logging origin-id hostname +# logging server 203.0.113.100 1 use-vrf management +# logging server 203.0.113.101 3 use-vrf default facility local6 + +- name: Delete all logging configuration + cisco.nxos.nxos_logging_global: + state: deleted + +# Task output +# ------------- +# before: +# console: +# severity: error +# facilities: +# - facility: auth +# severity: critical +# - facility: ftp +# severity: informational +# - facility: ospfv3 +# severity: alert +# ip: +# access_list: +# cache: +# entries: 16384 +# interval: 200 +# threshold: 5000 +# monitor: +# severity: warning +# origin_id: +# hostname: true +# hosts: +# - severity: alert +# host: 203.0.113.100 +# use_vrf: management +# - facility: local6 +# severity: error +# host: 203.0.113.101 +# use_vrf: default +# +# commands: +# - "logging console" +# - "logging monitor" +# - "no logging ip access-list cache entries 16384" +# - "no logging ip access-list cache interval 200" +# - "no logging ip access-list cache threshold 5000" +# - "no logging origin-id hostname" +# - "no logging severity auth 2" +# - "no logging severity ospfv3 1" +# - "no logging severity ftp 6" +# - "no logging server 203.0.113.100 1 use-vrf management" +# - "no logging server 203.0.113.101 3 facility local6 use-vrf default" +# +# after: {} + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_logging_global: + config: + console: + severity: error + monitor: + severity: warning + ip: + access_list: + cache: + entries: 16384 + interval: 200 + threshold: 5000 + facilities: + - facility: auth + severity: critical + - facility: ospfv3 + severity: alert + - facility: ftp + severity: informational + hosts: + - host: 203.0.113.100 + severity: alert + use_vrf: management + - host: 203.0.113.101 + severity: error + facility: local6 + use_vrf: default + origin_id: + hostname: True + +# Task Output (redacted) +# ----------------------- +# rendered: +# - "logging console 3" +# - "logging monitor 4" +# - "logging ip access-list cache entries 16384" +# - "logging ip access-list cache interval 200" +# - "logging ip access-list cache threshold 5000" +# - "logging severity auth 2" +# - "logging severity ospfv3 1" +# - "logging severity ftp 6" +# - "logging server 203.0.113.100 1 use-vrf management" +# - "logging server 203.0.113.101 3 facility local6 use-vrf default" +# - "logging origin-id hostname" + +# Using parsed + +# parsed.cfg +# ------------ +# logging console 3 +# logging monitor 4 +# logging ip access-list cache entries 16384 +# logging ip access-list cache interval 200 +# logging ip access-list cache threshold 5000 +# logging severity auth 2 +# logging severity ospfv3 1 +# logging severity ftp 6 +# logging origin-id hostname +# logging server 203.0.113.100 1 use-vrf management +# logging server 203.0.113.101 3 use-vrf default facility local6 + +- name: Parse externally provided logging configuration + cisco.nxos.nxos_logging_global: + running_config: "{{ lookup('file', './fixtures/parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# console: +# severity: error +# facilities: +# - facility: auth +# severity: critical +# - facility: ftp +# severity: informational +# - facility: ospfv3 +# severity: alert +# ip: +# access_list: +# cache: +# entries: 16384 +# interval: 200 +# threshold: 5000 +# monitor: +# severity: warning +# origin_id: +# hostname: true +# hosts: +# - severity: alert +# host: 203.0.113.100 +# use_vrf: management +# - facility: local6 +# severity: error +# host: 203.0.113.101 +# use_vrf: default +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when state is I(merged), I(replaced), I(overridden), I(deleted) or I(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when state is I(merged), I(replaced), I(overridden), I(deleted) or I(purged) + type: list + sample: + - "logging console 3" + - "logging monitor 4" + - "logging ip access-list cache entries 16384" + - "logging ip access-list cache interval 200" + - "logging ip access-list cache threshold 5000" +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when state is I(rendered) + type: list + sample: + - "logging ip access-list cache entries 4096" + - "no logging ip access-list cache interval 200" + - "no logging ip access-list cache threshold 5000" + - "no logging origin-id hostname" + - "logging origin-id ip 192.0.2.100" + - "logging server 198.51.100.101 1 port 6538 use-vrf management" +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when state is I(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when state is I(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.logging_global.logging_global import ( + Logging_globalArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.logging_global.logging_global import ( + Logging_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Logging_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Logging_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp.py new file mode 100644 index 00000000..046436d4 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp.py @@ -0,0 +1,446 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 = """ +module: nxos_ntp +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages core NTP configuration. +notes: +- Limited Support for Cisco MDS +description: +- Manages core NTP configuration. +version_added: 1.0.0 +deprecated: + alternative: nxos_ntp_global + why: Updated module released with more functionality. + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +options: + server: + description: + - Network address of NTP server. + type: str + peer: + description: + - Network address of NTP peer. + type: str + key_id: + description: + - Authentication key identifier to use with given NTP server or peer or keyword + 'default'. + type: str + prefer: + description: + - Makes given NTP server or peer the preferred NTP server or peer for the device. + choices: + - enabled + - disabled + type: str + vrf_name: + description: + - Makes the device communicate with the given NTP server or peer over a specific + VRF or keyword 'default'. + type: str + source_addr: + description: + - Local source address from which NTP messages are sent or keyword 'default'. + type: str + source_int: + description: + - Local source interface from which NTP messages are sent. Must be fully qualified + interface name or keyword 'default' + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# Set NTP Server with parameters +- cisco.nxos.nxos_ntp: + server: 1.2.3.4 + key_id: 32 + prefer: enabled + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"address": "192.0.2.2", "key_id": "48", + "peer_type": "server", "prefer": "enabled", + "source": "192.0.2.3", "source_type": "source"} +existing: + description: + - k/v pairs of existing ntp server/peer + returned: always + type: dict + sample: {"address": "192.0.2.2", "key_id": "32", + "peer_type": "server", "prefer": "enabled", + "source": "ethernet2/1", "source_type": "source-interface"} +end_state: + description: k/v pairs of ntp info after module execution + returned: always + type: dict + sample: {"address": "192.0.2.2", "key_id": "48", + "peer_type": "server", "prefer": "enabled", + "source": "192.0.2.3", "source_type": "source"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["ntp server 192.0.2.2 prefer key 48", + "no ntp source-interface ethernet2/1", "ntp source 192.0.2.3"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module, command_type="cli_show"): + if "show run" not in command: + output = "json" + else: + output = "text" + + commands = [{"command": command, "output": output}] + return run_commands(module, commands) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_ntp_source(module): + source_type = None + source = None + command = "show run | inc ntp.source" + output = execute_show_command(command, module, command_type="cli_show_ascii") + + if output: + try: + if "interface" in output[0]: + source_type = "source-interface" + else: + source_type = "source" + source = output[0].split()[2].lower() + except (AttributeError, IndexError): + source_type = None + source = None + + return source_type, source + + +def get_ntp_peer(module): + command = "show run | inc ntp.(server|peer)" + ntp_peer_list = [] + response = execute_show_command(command, module, command_type="cli_show_ascii") + + if response: + if isinstance(response, list): + ntp = response[0] + else: + ntp = response + if ntp: + ntp_regex = ( + r".*ntp\s(server\s(?P<address>\S+)|peer\s(?P<peer_address>\S+))" + r"\s*((?P<prefer>prefer)\s*)?(use-vrf\s(?P<vrf_name>\S+)\s*)?" + r"(key\s(?P<key_id>\d+))?.*" + ) + + split_ntp = ntp.splitlines() + for peer_line in split_ntp: + if "access-group" in peer_line: + continue + ntp_peer = {} + try: + peer_address = None + vrf_name = "default" + prefer = None + key_id = None + match_ntp = re.match(ntp_regex, peer_line, re.DOTALL) + group_ntp = match_ntp.groupdict() + + address = group_ntp["address"] + peer_address = group_ntp["peer_address"] + prefer = group_ntp["prefer"] + vrf_name = group_ntp["vrf_name"] + key_id = group_ntp["key_id"] + + if prefer is not None: + prefer = "enabled" + else: + prefer = "disabled" + + if address is not None: + peer_type = "server" + elif peer_address is not None: + peer_type = "peer" + address = peer_address + + args = dict( + peer_type=peer_type, + address=address, + prefer=prefer, + vrf_name=vrf_name, + key_id=key_id, + ) + + ntp_peer = dict((k, v) for k, v in args.items()) + ntp_peer_list.append(ntp_peer) + except AttributeError: + ntp_peer_list = [] + + return ntp_peer_list + + +def get_ntp_existing(address, peer_type, module): + peer_dict = {} + peer_server_list = [] + + peer_list = get_ntp_peer(module) + for peer in peer_list: + if peer["address"] == address: + peer_dict.update(peer) + else: + peer_server_list.append(peer) + + source_type, source = get_ntp_source(module) + + if source_type is not None and source is not None: + peer_dict["source_type"] = source_type + peer_dict["source"] = source + + return (peer_dict, peer_server_list) + + +def set_ntp_server_peer(peer_type, address, prefer, key_id, vrf_name): + command_strings = [] + + if prefer: + command_strings.append(" prefer") + if key_id: + command_strings.append(" key {0}".format(key_id)) + if vrf_name: + command_strings.append(" use-vrf {0}".format(vrf_name)) + + command_strings.insert(0, "ntp {0} {1}".format(peer_type, address)) + + command = "".join(command_strings) + + return command + + +def config_ntp(delta, existing): + if ( + delta.get("address") + or delta.get("peer_type") + or delta.get("vrf_name") + or delta.get("key_id") + or delta.get("prefer") + ): + address = delta.get("address", existing.get("address")) + peer_type = delta.get("peer_type", existing.get("peer_type")) + key_id = delta.get("key_id", existing.get("key_id")) + prefer = delta.get("prefer", existing.get("prefer")) + vrf_name = delta.get("vrf_name", existing.get("vrf_name")) + if delta.get("key_id") == "default": + key_id = None + else: + peer_type = None + prefer = None + + source_type = delta.get("source_type") + source = delta.get("source") + + if prefer: + if prefer == "enabled": + prefer = True + elif prefer == "disabled": + prefer = False + + if source: + source_type = delta.get("source_type", existing.get("source_type")) + + ntp_cmds = [] + if peer_type: + if existing.get("peer_type") and existing.get("address"): + ntp_cmds.append( + "no ntp {0} {1}".format(existing.get("peer_type"), existing.get("address")), + ) + ntp_cmds.append(set_ntp_server_peer(peer_type, address, prefer, key_id, vrf_name)) + if source: + existing_source_type = existing.get("source_type") + existing_source = existing.get("source") + if existing_source_type and source_type != existing_source_type: + ntp_cmds.append("no ntp {0} {1}".format(existing_source_type, existing_source)) + if source == "default": + if existing_source_type and existing_source: + ntp_cmds.append("no ntp {0} {1}".format(existing_source_type, existing_source)) + else: + ntp_cmds.append("ntp {0} {1}".format(source_type, source)) + + return ntp_cmds + + +def main(): + argument_spec = dict( + server=dict(type="str"), + peer=dict(type="str"), + key_id=dict(type="str"), + prefer=dict(type="str", choices=["enabled", "disabled"]), + vrf_name=dict(type="str"), + source_addr=dict(type="str"), + source_int=dict(type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[["server", "peer"], ["source_addr", "source_int"]], + supports_check_mode=True, + ) + + warnings = list() + + server = module.params["server"] or None + peer = module.params["peer"] or None + key_id = module.params["key_id"] + prefer = module.params["prefer"] + vrf_name = module.params["vrf_name"] + source_addr = module.params["source_addr"] + source_int = module.params["source_int"] + state = module.params["state"] + + if source_int is not None: + source_int = source_int.lower() + + if server: + peer_type = "server" + address = server + elif peer: + peer_type = "peer" + address = peer + else: + peer_type = None + address = None + + source_type = None + source = None + if source_addr: + source_type = "source" + source = source_addr + elif source_int: + source_type = "source-interface" + source = source_int + + if key_id or vrf_name or prefer: + if not server and not peer: + module.fail_json(msg="Please supply the server or peer parameter") + + args = dict( + peer_type=peer_type, + address=address, + key_id=key_id, + prefer=prefer, + vrf_name=vrf_name, + source_type=source_type, + source=source, + ) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + + existing, peer_server_list = get_ntp_existing(address, peer_type, module) + + end_state = existing + changed = False + commands = [] + + if state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta.get("key_id") and delta.get("key_id") == "default": + if not existing.get("key_id"): + delta.pop("key_id") + if delta: + command = config_ntp(delta, existing) + if command: + commands.append(command) + + elif state == "absent": + if existing.get("peer_type") and existing.get("address"): + command = "no ntp {0} {1}".format(existing["peer_type"], existing["address"]) + if command: + commands.append([command]) + + existing_source_type = existing.get("source_type") + existing_source = existing.get("source") + proposed_source_type = proposed.get("source_type") + proposed_source = proposed.get("source") + + if proposed_source_type: + if proposed_source_type == existing_source_type: + if proposed_source == existing_source: + command = "no ntp {0} {1}".format(existing_source_type, existing_source) + if command: + commands.append([command]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_ntp_existing(address, peer_type, module)[0] + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + results["end_state"] = end_state + results["peer_server_list"] = peer_server_list + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_auth.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_auth.py new file mode 100644 index 00000000..3e564381 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_auth.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_ntp_auth +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages NTP authentication. +description: +- Manages NTP authentication. +version_added: 1.0.0 +deprecated: + alternative: nxos_ntp_global + why: Updated module released with more functionality. + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- If C(state=absent), the module will remove the given key configuration if it exists. +- If C(state=absent) and C(authentication=on), authentication will be turned off. +options: + key_id: + description: + - Authentication key identifier (numeric). + type: str + md5string: + description: + - MD5 String. + type: str + auth_type: + description: + - Whether the given md5string is in cleartext or has been encrypted. If in cleartext, + the device will encrypt it before storing it. + default: text + choices: + - text + - encrypt + type: str + trusted_key: + description: + - Whether the given key is required to be supplied by a time source for the device + to synchronize to the time source. + choices: + - 'false' + - 'true' + default: 'false' + type: str + authentication: + description: + - Turns NTP authentication on or off. + choices: + - "on" + - "off" + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# Basic NTP authentication configuration +- cisco.nxos.nxos_ntp_auth: + key_id: 32 + md5string: hello + auth_type: text +""" + +RETURN = """ +commands: + description: command sent to the device + returned: always + type: list + sample: ["ntp authentication-key 32 md5 helloWorld 0", "ntp trusted-key 32"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + if "show run" not in command: + command = {"command": command, "output": "json"} + else: + command = {"command": command, "output": "text"} + + return run_commands(module, [command]) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_ntp_auth(module): + command = "show ntp authentication-status" + + body = execute_show_command(command, module)[0] + ntp_auth_str = body["authentication"] + + if "enabled" in ntp_auth_str: + ntp_auth = True + else: + ntp_auth = False + + return ntp_auth + + +def get_ntp_trusted_key(module): + trusted_key_list = [] + command = "show run | inc ntp.trusted-key" + + trusted_key_str = execute_show_command(command, module)[0] + if trusted_key_str: + trusted_keys = trusted_key_str.splitlines() + + else: + trusted_keys = [] + + for line in trusted_keys: + if line: + trusted_key_list.append(str(line.split()[2])) + + return trusted_key_list + + +def get_ntp_auth_key(key_id, module): + authentication_key = {} + command = "show run | inc ntp.authentication-key.{0}".format(key_id) + auth_regex = ( + r".*ntp\sauthentication-key\s(?P<key_id>\d+)\smd5\s(?P<md5string>\S+)\s(?P<atype>\S+).*" + ) + + body = execute_show_command(command, module)[0] + + try: + match_authentication = re.match(auth_regex, body, re.DOTALL) + group_authentication = match_authentication.groupdict() + authentication_key["key_id"] = group_authentication["key_id"] + authentication_key["md5string"] = group_authentication["md5string"] + if group_authentication["atype"] == "7": + authentication_key["auth_type"] = "encrypt" + else: + authentication_key["auth_type"] = "text" + except (AttributeError, TypeError): + authentication_key = {} + + return authentication_key + + +def get_ntp_auth_info(key_id, module): + auth_info = get_ntp_auth_key(key_id, module) + trusted_key_list = get_ntp_trusted_key(module) + auth_power = get_ntp_auth(module) + + if key_id in trusted_key_list: + auth_info["trusted_key"] = "true" + else: + auth_info["trusted_key"] = "false" + + if auth_power: + auth_info["authentication"] = "on" + else: + auth_info["authentication"] = "off" + + return auth_info + + +def auth_type_to_num(auth_type): + if auth_type == "encrypt": + return "7" + else: + return "0" + + +def set_ntp_auth_key(key_id, md5string, auth_type, trusted_key, authentication): + ntp_auth_cmds = [] + if key_id and md5string: + auth_type_num = auth_type_to_num(auth_type) + ntp_auth_cmds.append( + "ntp authentication-key {0} md5 {1} {2}".format(key_id, md5string, auth_type_num), + ) + + if trusted_key == "true": + ntp_auth_cmds.append("ntp trusted-key {0}".format(key_id)) + elif trusted_key == "false": + ntp_auth_cmds.append("no ntp trusted-key {0}".format(key_id)) + + if authentication == "on": + ntp_auth_cmds.append("ntp authenticate") + elif authentication == "off": + ntp_auth_cmds.append("no ntp authenticate") + + return ntp_auth_cmds + + +def remove_ntp_auth_key(key_id, md5string, auth_type, trusted_key, authentication): + auth_remove_cmds = [] + if key_id: + auth_type_num = auth_type_to_num(auth_type) + auth_remove_cmds.append( + "no ntp authentication-key {0} md5 {1} {2}".format(key_id, md5string, auth_type_num), + ) + + if authentication: + auth_remove_cmds.append("no ntp authenticate") + return auth_remove_cmds + + +def main(): + argument_spec = dict( + key_id=dict(type="str"), + md5string=dict(type="str"), + auth_type=dict(choices=["text", "encrypt"], default="text"), + trusted_key=dict(choices=["true", "false"], default="false"), + authentication=dict(choices=["on", "off"]), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + key_id = module.params["key_id"] + md5string = module.params["md5string"] + auth_type = module.params["auth_type"] + trusted_key = module.params["trusted_key"] + authentication = module.params["authentication"] + state = module.params["state"] + + if key_id: + if not trusted_key and not md5string: + module.fail_json(msg="trusted_key or md5string MUST be specified") + + args = dict( + key_id=key_id, + md5string=md5string, + auth_type=auth_type, + trusted_key=trusted_key, + authentication=authentication, + ) + + changed = False + proposed = dict((k, v) for k, v in args.items() if v is not None) + + existing = get_ntp_auth_info(key_id, module) + end_state = existing + + delta = dict(set(proposed.items()).difference(existing.items())) + + commands = [] + if state == "present": + if delta: + command = set_ntp_auth_key( + key_id, + md5string, + delta.get("auth_type"), + delta.get("trusted_key"), + delta.get("authentication"), + ) + if command: + commands.append(command) + elif state == "absent": + auth_toggle = None + if existing.get("authentication") == "on": + auth_toggle = True + if not existing.get("key_id"): + key_id = None + command = remove_ntp_auth_key(key_id, md5string, auth_type, trusted_key, auth_toggle) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + load_config(module, cmds) + end_state = get_ntp_auth_info(key_id, module) + delta = dict(set(end_state.items()).difference(existing.items())) + if delta or (len(existing) != len(end_state)): + changed = True + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + results["end_state"] = end_state + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_global.py new file mode 100644 index 00000000..e99fbef8 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_global.py @@ -0,0 +1,736 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_ntp_global +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_ntp_global +short_description: NTP Global resource module. +description: +- This module manages ntp configuration on devices running Cisco NX-OS. +version_added: 2.6.0 +notes: +- Tested against NX-OS 9.3.6 on Cisco Nexus Switches. +- This module works with connection C(network_cli) and C(httpapi). +- Tested against Cisco MDS NX-OS 9.2(2) with connection C(network_cli). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config ntp). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dict of ntp configuration. + type: dict + suboptions: + access_group: + description: + - NTP access-group. + - This option is unsupported on MDS switches. + type: dict + suboptions: + match_all: + description: Scan ACLs present in all ntp access groups. + type: bool + peer: + description: Access-group peer. + type: list + elements: dict + suboptions: + access_list: + description: Name of access list. + type: str + query_only: + description: Access-group query-only. + type: list + elements: dict + suboptions: + access_list: + description: Name of access list. + type: str + serve: + description: Access-group serve. + type: list + elements: dict + suboptions: + access_list: + description: Name of access list. + type: str + serve_only: + description: Access-group serve-only. + type: list + elements: dict + suboptions: + access_list: + description: Name of access list. + type: str + allow: + description: Enable/Disable the packets. + type: dict + suboptions: + control: + description: Control mode packets. + type: dict + suboptions: + rate_limit: + description: Rate-limit delay. + type: int + private: + description: Enable/Disable Private mode packets. + type: bool + authenticate: + description: Enable/Disable authentication. + type: bool + authentication_keys: + description: NTP authentication key. + type: list + elements: dict + suboptions: + id: + description: Authentication key number (range 1-65535). + type: int + key: + description: Authentication key. + type: str + encryption: + description: + - 0 for Clear text + - 7 for Encrypted + type: int + logging: + description: Enable/Disable logging of NTPD Events. + type: bool + master: + description: + - Act as NTP master clock. + - This option is unsupported on MDS switches. + type: dict + suboptions: + stratum: + description: Stratum number. + type: int + passive: + description: + - NTP passive command. + - This option is unsupported on MDS switches. + type: bool + peers: + description: NTP Peers. + type: list + elements: dict + suboptions: + peer: + description: Hostname/IP address of the NTP Peer. + type: str + key_id: + description: Keyid to be used while communicating to this server. + type: int + maxpoll: + description: + - Maximum interval to poll a peer. + - Poll interval in secs to a power of 2. + type: int + minpoll: + description: + - Minimum interval to poll a peer. + - Poll interval in secs to a power of 2. + type: int + prefer: + description: + - Preferred Server. + type: bool + vrf: + description: + - Display per-VRF information. + - This option is unsupported on MDS switches. + type: str + aliases: ["use_vrf"] + servers: + description: NTP servers. + type: list + elements: dict + suboptions: + server: + description: Hostname/IP address of the NTP Peer. + type: str + key_id: + description: Keyid to be used while communicating to this server. + type: int + maxpoll: + description: + - Maximum interval to poll a peer. + - Poll interval in secs to a power of 2. + type: int + minpoll: + description: + - Minimum interval to poll a peer. + - Poll interval in secs to a power of 2. + type: int + prefer: + description: + - Preferred Server. + type: bool + vrf: + description: + - Display per-VRF information. + - This option is not applicable for MDS switches. + type: str + aliases: ["use_vrf"] + source: + description: + - Source of NTP packets. + - This option is unsupported on MDS switches. + type: str + source_interface: + description: Source interface sending NTP packets. + type: str + trusted_keys: + description: NTP trusted-key number. + type: list + elements: dict + suboptions: + key_id: + description: Trusted-Key number. + type: int + state: + description: + - The state the configuration should be left in. + - The states I(replaced) and I(overridden) have identical + behaviour for this module. + - Please refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" + +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config ntp +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_ntp_global: &id001 + config: + access_group: + peer: + - access_list: PeerAcl1 + serve: + - access_list: ServeAcl1 + authenticate: True + authentication_keys: + - id: 1001 + key: vagwwtKfkv + encryption: 7 + - id: 1002 + key: vagwwtKfkvgthz + encryption: 7 + logging: True + master: + stratum: 2 + peers: + - peer: 192.0.2.1 + key_id: 1 + maxpoll: 15 + minpoll: 5 + vrf: default + - peer: 192.0.2.2 + key_id: 2 + prefer: True + vrf: siteA + servers: + - server: 198.51.100.1 + key_id: 2 + vrf: default + - server: 203.0.113.1 + key_id: 1 + vrf: siteB + +# Task output +# ------------- +# before: {} +# +# commands: +# - "ntp authenticate" +# - "ntp logging" +# - "ntp master 2" +# - "ntp authentication-keys 1001 md5 vagwwtKfkv 7" +# - "ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7" +# - "ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15" +# - "ntp peer 192.0.2.2 prefer use-vrf siteA key 2" +# - "ntp server 198.51.100.1 use-vrf default key 2" +# - "ntp server 203.0.113.1 use-vrf siteB key 1" +# - "ntp access-group peer PeerAcl1" +# - "ntp access-group serve ServeAcl1" +# +# after: +# access_group: +# peer: +# - access_list: PeerAcl1 +# serve: +# - access_list: ServeAcl1 +# authenticate: True +# authentication_keys: +# - id: 1001 +# key: vagwwtKfkv +# encryption: 7 +# - id: 1002 +# key: vagwwtKfkvgthz +# encryption: 7 +# logging: True +# master: +# stratum: 2 +# peers: +# - peer: 192.0.2.1 +# key_id: 1 +# maxpoll: 15 +# minpoll: 5 +# vrf: default +# - peer: 192.0.2.2 +# key_id: 2 +# prefer: True +# vrf: siteA +# servers: +# - server: 198.51.100.1 +# key_id: 2 +# vrf: default +# - server: 203.0.113.1 +# key_id: 1 +# vrf: siteB + +# After state: +# ------------ +# nxos-9k-rdo# show running-config ntp +# ntp authenticate +# ntp logging +# ntp master 2 +# ntp authentication-keys 1001 md5 vagwwtKfkv 7 +# ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7 +# ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15 +# ntp peer 192.0.2.2 prefer use-vrf siteA key 2 +# ntp server 198.51.100.1 use-vrf default key 2 +# ntp server 203.0.113.1 use-vrf siteB key 1 +# ntp access-group peer PeerAcl1 +# ntp access-group serve ServeAcl1 + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config ntp +# ntp authenticate +# ntp logging +# ntp master 2 +# ntp authentication-keys 1001 md5 vagwwtKfkv 7 +# ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7 +# ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15 +# ntp peer 192.0.2.2 prefer use-vrf siteA key 2 +# ntp server 198.51.100.1 use-vrf default key 2 +# ntp server 203.0.113.1 use-vrf siteB key 1 +# ntp access-group peer PeerAcl1 +# ntp access-group serve ServeAcl1 + +- name: Replace logging global configurations of listed logging global with provided configurations + cisco.nxos.nxos_ntp_global: + config: + access_group: + peer: + - access_list: PeerAcl2 + serve: + - access_list: ServeAcl2 + logging: True + master: + stratum: 2 + peers: + - peer: 192.0.2.1 + key_id: 1 + maxpoll: 15 + minpoll: 5 + vrf: default + - peer: 192.0.2.5 + key_id: 2 + prefer: True + vrf: siteA + servers: + - server: 198.51.100.1 + key_id: 2 + vrf: default + state: replaced + +# Task output +# ------------- +# before: +# access_group: +# peer: +# - access_list: PeerAcl1 +# serve: +# - access_list: ServeAcl1 +# authenticate: True +# authentication_keys: +# - id: 1001 +# key: vagwwtKfkv +# encryption: 7 +# - id: 1002 +# key: vagwwtKfkvgthz +# encryption: 7 +# logging: True +# master: +# stratum: 2 +# peers: +# - peer: 192.0.2.1 +# key_id: 1 +# maxpoll: 15 +# minpoll: 5 +# vrf: default +# - peer: 192.0.2.2 +# key_id: 2 +# prefer: True +# vrf: siteA +# servers: +# - server: 198.51.100.1 +# key_id: 2 +# vrf: default +# - server: 203.0.113.1 +# key_id: 1 +# vrf: siteB +# +# commands: +# - "no ntp authenticate" +# - "no ntp authentication-keys 1001 md5 vagwwtKfkv 7" +# - "no ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7" +# - "ntp peer 192.0.2.5 prefer use-vrf siteA key 2" +# - "no ntp peer 192.0.2.2 prefer use-vrf siteA key 2" +# - "no ntp server 203.0.113.1 use-vrf siteB key 1" +# - "ntp access-group peer PeerAcl2" +# - "no ntp access-group peer PeerAcl1" +# - "ntp access-group serve ServeAcl2" +# - "no ntp access-group serve ServeAcl1" +# +# after: +# access_group: +# peer: +# - access_list: PeerAcl2 +# serve: +# - access_list: ServeAcl2 +# logging: True +# master: +# stratum: 2 +# peers: +# - peer: 192.0.2.1 +# key_id: 1 +# maxpoll: 15 +# minpoll: 5 +# vrf: default +# - peer: 192.0.2.5 +# key_id: 2 +# prefer: True +# vrf: siteA +# servers: +# - server: 198.51.100.1 +# key_id: 2 +# vrf: default + +# After state: +# ------------ +# nxos-9k-rdo# show running-config ntp +# ntp logging +# ntp master 2 +# ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15 +# ntp peer 192.0.2.5 prefer use-vrf siteA key 2 +# ntp server 198.51.100.1 use-vrf default key 2 +# ntp access-group peer PeerAcl2 +# ntp access-group serve ServeAcl2 + +# Using deleted to delete all logging configurations + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config ntp + +- name: Delete all logging configuration + cisco.nxos.nxos_ntp_global: + state: deleted + +# Task output +# ------------- +# before: +# access_group: +# peer: +# - access_list: PeerAcl1 +# serve: +# - access_list: ServeAcl1 +# authenticate: True +# authentication_keys: +# - id: 1001 +# key: vagwwtKfkv +# encryption: 7 +# - id: 1002 +# key: vagwwtKfkvgthz +# encryption: 7 +# logging: True +# master: +# stratum: 2 +# peers: +# - peer: 192.0.2.1 +# key_id: 1 +# maxpoll: 15 +# minpoll: 5 +# vrf: default +# - peer: 192.0.2.2 +# key_id: 2 +# prefer: True +# vrf: siteA +# servers: +# - server: 198.51.100.1 +# key_id: 2 +# vrf: default +# - server: 203.0.113.1 +# key_id: 1 +# vrf: siteB +# +# commands: +# - "no ntp authenticate" +# - "no ntp logging" +# - "no ntp master 2" +# - "no ntp authentication-keys 1001 md5 vagwwtKfkv 7" +# - "no ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7" +# - "no ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15" +# - "no ntp peer 192.0.2.2 prefer use-vrf siteA key 2" +# - "no ntp server 198.51.100.1 use-vrf default key 2" +# - "no ntp server 203.0.113.1 use-vrf siteB key 1" +# - "no ntp access-group peer PeerAcl1" +# - "no ntp access-group serve ServeAcl1" +# +# after: {} + +# After state: +# ------------ +# nxos-9k-rdo# show running-config ntp +# nxos-9k-rdo# + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_ntp_global: + config: + access_group: + peer: + - access_list: PeerAcl1 + serve: + - access_list: ServeAcl1 + authenticate: True + authentication_keys: + - id: 1001 + key: vagwwtKfkv + encryption: 7 + - id: 1002 + key: vagwwtKfkvgthz + encryption: 7 + logging: True + master: + stratum: 2 + peers: + - peer: 192.0.2.1 + key_id: 1 + maxpoll: 15 + minpoll: 5 + vrf: default + - peer: 192.0.2.2 + key_id: 2 + prefer: True + vrf: siteA + servers: + - server: 198.51.100.1 + key_id: 2 + vrf: default + - server: 203.0.113.1 + key_id: 1 + vrf: siteB + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - "ntp authenticate" +# - "ntp logging" +# - "ntp master 2" +# - "ntp authentication-keys 1001 md5 vagwwtKfkv 7" +# - "ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7" +# - "ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15" +# - "ntp peer 192.0.2.2 prefer use-vrf siteA key 2" +# - "ntp server 198.51.100.1 use-vrf default key 2" +# - "ntp server 203.0.113.1 use-vrf siteB key 1" +# - "ntp access-group peer PeerAcl1" +# - "ntp access-group serve ServeAcl1" + +# Using parsed + +# parsed.cfg +# ------------ +# ntp authenticate +# ntp logging +# ntp master 2 +# ntp authentication-keys 1001 md5 vagwwtKfkv 7 +# ntp authentication-keys 1002 md5 vagwwtKfkvgthz 7 +# ntp peer 192.0.2.1 use-vrf default key 1 minpoll 5 maxpoll 15 +# ntp peer 192.0.2.2 prefer use-vrf siteA key 2 +# ntp server 198.51.100.1 use-vrf default key 2 +# ntp server 203.0.113.1 use-vrf siteB key 1 +# ntp access-group peer PeerAcl1 +# ntp access-group serve ServeAcl1 + +- name: Parse externally provided ntp configuration + cisco.nxos.nxos_ntp_global: + running_config: "{{ lookup('file', './fixtures/parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# access_group: +# peer: +# - access_list: PeerAcl1 +# serve: +# - access_list: ServeAcl1 +# authenticate: True +# authentication_keys: +# - id: 1001 +# key: vagwwtKfkv +# encryption: 7 +# - id: 1002 +# key: vagwwtKfkvgthz +# encryption: 7 +# logging: True +# master: +# stratum: 2 +# peers: +# - peer: 192.0.2.1 +# key_id: 1 +# maxpoll: 15 +# minpoll: 5 +# vrf: default +# - peer: 192.0.2.2 +# key_id: 2 +# prefer: True +# vrf: siteA +# servers: +# - server: 198.51.100.1 +# key_id: 2 +# vrf: default +# - server: 203.0.113.1 +# key_id: 1 +# vrf: siteB +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - ntp master stratum 2 + - ntp peer 198.51.100.1 use-vrf test maxpoll 7 + - ntp authentication-key 10 md5 wawyhanx2 7 + - ntp access-group peer PeerAcl1 + - ntp access-group peer PeerAcl2 + - ntp access-group query-only QueryAcl1 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - ntp master stratum 2 + - ntp peer 198.51.100.1 use-vrf test maxpoll 7 + - ntp authentication-key 10 md5 wawyhanx2 7 + - ntp access-group peer PeerAcl1 + - ntp access-group peer PeerAcl2 + - ntp access-group query-only QueryAcl1 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.ntp_global.ntp_global import ( + Ntp_globalArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.ntp_global.ntp_global import ( + Ntp_global, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ntp_globalArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Ntp_global(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_options.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_options.py new file mode 100644 index 00000000..28fd1aac --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ntp_options.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_ntp_options +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages NTP options. +description: +- Manages NTP options, e.g. authoritative server and logging. +version_added: 1.0.0 +deprecated: + alternative: nxos_ntp_global + why: Updated module released with more functionality. + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- When C(state=absent), master and logging will be set to False and stratum will be + removed as well +options: + master: + description: + - Sets whether the device is an authoritative NTP server. + type: bool + stratum: + description: + - If C(master=true), an optional stratum can be supplied (1-15). The device default + is 8. + type: str + logging: + description: + - Sets whether NTP logging is enabled on the device. + type: bool + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" +EXAMPLES = """ +# Basic NTP options configuration +- cisco.nxos.nxos_ntp_options: + master: true + stratum: 12 + logging: false + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +updates: + description: command sent to the device + returned: always + type: list + sample: ["no ntp logging", "ntp master 12"] +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def get_current(module): + cmd = "show running-config | inc ntp" + + master = False + logging = False + stratum = None + + output = run_commands(module, ({"command": cmd, "output": "text"}))[0] + + if output: + match = re.search(r"^ntp master(?: (\d+))", output, re.M) + if match: + master = True + stratum = match.group(1) + logging = "ntp logging" in output.lower() + + return {"master": master, "stratum": stratum, "logging": logging} + + +def main(): + argument_spec = dict( + master=dict(required=False, type="bool"), + stratum=dict(required=False, type="str"), + logging=dict(required=False, type="bool"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + master = module.params["master"] + stratum = module.params["stratum"] + logging = module.params["logging"] + state = module.params["state"] + + if stratum and master is False: + if stratum != 8: + module.fail_json(msg="master MUST be True when stratum is changed") + + current = get_current(module) + + result = {"changed": False} + + commands = list() + + if state == "absent": + if current["master"]: + commands.append("no ntp master") + if current["logging"]: + commands.append("no ntp logging") + + elif state == "present": + if master and not current["master"]: + commands.append("ntp master") + elif master is False and current["master"]: + commands.append("no ntp master") + if stratum and stratum != current["stratum"]: + commands.append("ntp master %s" % stratum) + + if logging and not current["logging"]: + commands.append("ntp logging") + elif logging is False and current["logging"]: + commands.append("no ntp logging") + + result["commands"] = commands + result["updates"] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + result["warnings"] = warnings + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_nxapi.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_nxapi.py new file mode 100644 index 00000000..cfd7cc87 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_nxapi.py @@ -0,0 +1,424 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# pylint: skip-file +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_nxapi +extends_documentation_fragment: +- cisco.nxos.nxos +author: Peter Sprygada (@privateip) +short_description: Manage NXAPI configuration on an NXOS device. +notes: +- Limited Support for Cisco MDS +description: +- Configures the NXAPI feature on devices running Cisco NXOS. The NXAPI feature is + absent from the configuration by default. Since this module manages the NXAPI feature + it only supports the use of the C(Cli) transport. +version_added: 1.0.0 +options: + http_port: + description: + - Configure the port with which the HTTP server will listen on for requests. By + default, NXAPI will bind the HTTP service to the standard HTTP port 80. This + argument accepts valid port values in the range of 1 to 65535. + required: false + default: 80 + type: int + http: + description: + - Controls the operating state of the HTTP protocol as one of the underlying transports + for NXAPI. By default, NXAPI will enable the HTTP transport when the feature + is first configured. To disable the use of the HTTP transport, set the value + of this argument to False. + required: false + default: true + type: bool + aliases: + - enable_http + https_port: + description: + - Configure the port with which the HTTPS server will listen on for requests. By + default, NXAPI will bind the HTTPS service to the standard HTTPS port 443. This + argument accepts valid port values in the range of 1 to 65535. + required: false + default: 443 + type: int + https: + description: + - Controls the operating state of the HTTPS protocol as one of the underlying + transports for NXAPI. By default, NXAPI will disable the HTTPS transport when + the feature is first configured. To enable the use of the HTTPS transport, + set the value of this argument to True. + required: false + default: false + type: bool + aliases: + - enable_https + sandbox: + description: + - The NXAPI feature provides a web base UI for developers for entering commands. This + feature is initially disabled when the NXAPI feature is configured for the first + time. When the C(sandbox) argument is set to True, the developer sandbox URL + will accept requests and when the value is set to False, the sandbox URL is + unavailable. This is supported on NX-OS 7K series. + required: false + type: bool + aliases: + - enable_sandbox + state: + description: + - The C(state) argument controls whether or not the NXAPI feature is configured + on the remote device. When the value is C(present) the NXAPI feature configuration + is present in the device running-config. When the values is C(absent) the feature + configuration is removed from the running-config. + choices: + - present + - absent + required: false + default: present + type: str + ssl_strong_ciphers: + description: + - Controls the use of whether strong or weak ciphers are configured. By default, + this feature is disabled and weak ciphers are configured. To enable the use + of strong ciphers, set the value of this argument to True. + required: false + default: false + type: bool + tlsv1_0: + description: + - Controls the use of the Transport Layer Security version 1.0 is configured. By + default, this feature is enabled. To disable the use of TLSV1.0, set the value + of this argument to True. + required: false + default: true + type: bool + tlsv1_1: + description: + - Controls the use of the Transport Layer Security version 1.1 is configured. By + default, this feature is disabled. To enable the use of TLSV1.1, set the value + of this argument to True. + required: false + default: false + type: bool + tlsv1_2: + description: + - Controls the use of the Transport Layer Security version 1.2 is configured. By + default, this feature is disabled. To enable the use of TLSV1.2, set the value + of this argument to True. + required: false + default: false + type: bool +""" + +EXAMPLES = """ +- name: Enable NXAPI access with default configuration + cisco.nxos.nxos_nxapi: + state: present + +- name: Enable NXAPI with no HTTP, HTTPS at port 9443 and sandbox disabled + cisco.nxos.nxos_nxapi: + enable_http: false + https_port: 9443 + https: yes + enable_sandbox: no + +- name: remove NXAPI configuration + cisco.nxos.nxos_nxapi: + state: absent +""" + +RETURN = """ +updates: + description: + - Returns the list of commands that need to be pushed into the remote + device to satisfy the arguments + returned: always + type: list + sample: ['no feature nxapi'] +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.utils.utils import Version + + +def check_args(module, warnings, capabilities): + network_api = capabilities.get("network_api", "nxapi") + if network_api == "nxapi": + module.fail_json(msg="module not supported over nxapi transport") + + os_platform = capabilities["device_info"]["network_os_platform"] + if "7K" not in os_platform and module.params["sandbox"]: + module.fail_json( + msg="sandbox or enable_sandbox is supported on NX-OS 7K series of switches", + ) + + state = module.params["state"] + + if state == "started": + module.params["state"] = "present" + warnings.append( + "state=started is deprecated and will be removed in a " + "a future release. Please use state=present instead", + ) + elif state == "stopped": + module.params["state"] = "absent" + warnings.append( + "state=stopped is deprecated and will be removed in a " + "a future release. Please use state=absent instead", + ) + + for key in ["http_port", "https_port"]: + if module.params[key] is not None: + if not 1 <= module.params[key] <= 65535: + module.fail_json(msg="%s must be between 1 and 65535" % key) + + return warnings + + +def map_obj_to_commands(want, have, module, warnings, capabilities): + send_commands = list() + commands = dict() + os_platform = None + os_version = None + + device_info = capabilities.get("device_info") + if device_info: + os_version = device_info.get("network_os_version") + if os_version: + os_version = os_version[:3] + os_platform = device_info.get("network_os_platform") + if os_platform: + os_platform = os_platform[:3] + + def needs_update(x): + return want.get(x) is not None and (want.get(x) != have.get(x)) + + if needs_update("state"): + if want["state"] == "absent": + return ["no feature nxapi"] + send_commands.append("feature nxapi") + elif want["state"] == "absent": + return send_commands + + for parameter in ["http", "https"]: + port_param = parameter + "_port" + if needs_update(parameter): + if want.get(parameter) is False: + commands[parameter] = "no nxapi %s" % parameter + else: + commands[parameter] = "nxapi %s port %s" % ( + parameter, + want.get(port_param), + ) + + if needs_update(port_param) and want.get(parameter) is True: + commands[parameter] = "nxapi %s port %s" % ( + parameter, + want.get(port_param), + ) + + if needs_update("sandbox"): + commands["sandbox"] = "nxapi sandbox" + if not want["sandbox"]: + commands["sandbox"] = "no %s" % commands["sandbox"] + + if os_platform and os_version: + if (os_platform == "N9K" or os_platform == "N3K") and Version(os_version) >= "9.2": + if needs_update("ssl_strong_ciphers"): + commands["ssl_strong_ciphers"] = "nxapi ssl ciphers weak" + if want["ssl_strong_ciphers"] is True: + commands["ssl_strong_ciphers"] = "no nxapi ssl ciphers weak" + + have_ssl_protocols = "" + want_ssl_protocols = "" + for key, value in { + "tlsv1_2": "TLSv1.2", + "tlsv1_1": "TLSv1.1", + "tlsv1_0": "TLSv1", + }.items(): + if needs_update(key): + if want.get(key) is True: + want_ssl_protocols = " ".join([want_ssl_protocols, value]) + elif have.get(key) is True: + have_ssl_protocols = " ".join([have_ssl_protocols, value]) + + if len(want_ssl_protocols) > 0: + commands["ssl_protocols"] = "nxapi ssl protocols%s" % ( + " ".join([want_ssl_protocols, have_ssl_protocols]) + ) + else: + warnings.append( + "os_version and/or os_platform keys from " + "platform capabilities are not available. " + "Any NXAPI SSL optional arguments will be ignored", + ) + + send_commands.extend(commands.values()) + + return send_commands + + +def parse_http(data): + http_res = [r"nxapi http port (\d+)"] + http_port = None + + for regex in http_res: + match = re.search(regex, data, re.M) + if match: + http_port = int(match.group(1)) + break + + return {"http": http_port is not None, "http_port": http_port} + + +def parse_https(data): + https_res = [r"nxapi https port (\d+)"] + https_port = None + + for regex in https_res: + match = re.search(regex, data, re.M) + if match: + https_port = int(match.group(1)) + break + + return {"https": https_port is not None, "https_port": https_port} + + +def parse_sandbox(data): + sandbox = [item for item in data.split("\n") if re.search(r".*sandbox.*", item)] + value = False + if sandbox and sandbox[0] == "nxapi sandbox": + value = True + return {"sandbox": value} + + +def parse_ssl_strong_ciphers(data): + ciphers_res = [r"(\w+) nxapi ssl ciphers weak"] + value = None + + for regex in ciphers_res: + match = re.search(regex, data, re.M) + if match: + value = match.group(1) + break + + return {"ssl_strong_ciphers": value == "no"} + + +def parse_ssl_protocols(data): + tlsv1_0 = re.search(r"(?<!\S)TLSv1(?!\S)", data, re.M) is not None + tlsv1_1 = re.search(r"(?<!\S)TLSv1.1(?!\S)", data, re.M) is not None + tlsv1_2 = re.search(r"(?<!\S)TLSv1.2(?!\S)", data, re.M) is not None + + return {"tlsv1_0": tlsv1_0, "tlsv1_1": tlsv1_1, "tlsv1_2": tlsv1_2} + + +def map_config_to_obj(module): + out = run_commands(module, ["show run all | inc nxapi"], check_rc=False)[0] + match = re.search(r"no feature nxapi", out, re.M) + # There are two possible outcomes when nxapi is disabled on nxos platforms. + # 1. Nothing is displayed in the running config. + # 2. The 'no feature nxapi' command is displayed in the running config. + if match or out == "": + return {"state": "absent"} + + out = str(out).strip() + + obj = {"state": "present"} + obj.update(parse_http(out)) + obj.update(parse_https(out)) + obj.update(parse_sandbox(out)) + obj.update(parse_ssl_strong_ciphers(out)) + obj.update(parse_ssl_protocols(out)) + + return obj + + +def map_params_to_obj(module): + obj = { + "http": module.params["http"], + "http_port": module.params["http_port"], + "https": module.params["https"], + "https_port": module.params["https_port"], + "sandbox": module.params["sandbox"], + "state": module.params["state"], + "ssl_strong_ciphers": module.params["ssl_strong_ciphers"], + "tlsv1_0": module.params["tlsv1_0"], + "tlsv1_1": module.params["tlsv1_1"], + "tlsv1_2": module.params["tlsv1_2"], + } + + return obj + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + http=dict(aliases=["enable_http"], type="bool", default=True), + http_port=dict(type="int", default=80), + https=dict(aliases=["enable_https"], type="bool", default=False), + https_port=dict(type="int", default=443), + sandbox=dict(aliases=["enable_sandbox"], type="bool"), + state=dict(default="present", choices=["present", "absent"]), + ssl_strong_ciphers=dict(type="bool", default=False), + tlsv1_0=dict(type="bool", default=True), + tlsv1_1=dict(type="bool", default=False), + tlsv1_2=dict(type="bool", default=False), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + warning_msg = "Module nxos_nxapi currently defaults to configure 'http port 80'. " + warning_msg += "Default behavior is changing to configure 'https port 443'" + warning_msg += " when params 'http, http_port, https, https_port' are not set in the playbook" + module.deprecate(msg=warning_msg, date="2022-06-01", collection_name="cisco.nxos") + + capabilities = get_capabilities(module) + + check_args(module, warnings, capabilities) + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(want, have, module, warnings, capabilities) + + result = {"changed": False, "warnings": warnings, "commands": commands} + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ospf_interfaces.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospf_interfaces.py new file mode 100644 index 00000000..c0a754c7 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospf_interfaces.py @@ -0,0 +1,1455 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_ospf_interfaces +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_ospf_interfaces +version_added: 1.3.0 +short_description: OSPF Interfaces Resource Module. +description: +- This module manages OSPF(v2/v3) configuration of interfaces on devices running Cisco NX-OS. +notes: +- Unsupported for Cisco MDS +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section "^interface"). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of OSPF configuration for interfaces. + type: list + elements: dict + suboptions: + name: + description: + - Name/Identifier of the interface. + type: str + required: True + address_family: + description: + - OSPF settings on the interfaces in address-family context. + type: list + elements: dict + suboptions: + afi: + description: + - Address Family Identifier (AFI) for OSPF settings on the interfaces. + type: str + choices: ['ipv4', 'ipv6'] + required: True + processes: + description: + - Interfaces configuration for an OSPF process. + type: list + elements: dict + suboptions: + process_id: + description: + - OSPF process tag. + type: str + required: True + area: + description: + - Area associated with interface. + type: dict + suboptions: + area_id: + description: + - Area ID in IP address format. + type: str + required: True + secondaries: + description: + - Do not include secondary IPv4/IPv6 addresses. + type: bool + multi_areas: + description: + - Multi-Areas associated with interface. + - Valid values are Area Ids as an integer or IP address. + type: list + elements: str + multi_areas: + description: + - Multi-Areas associated with interface (not tied to OSPF process). + - Valid values are Area Ids as an integer or IP address. + type: list + elements: str + authentication: + description: + - Authentication settings on the interface. + type: dict + suboptions: + key_chain: + description: + - Authentication password key-chain. + type: str + message_digest: + description: + - Use message-digest authentication. + type: bool + enable: + description: + - Enable/disable authentication on the interface. + type: bool + null_auth: + description: + - Use null(disable) authentication. + type: bool + authentication_key: + description: + - Configure the authentication key for the interface. + type: dict + suboptions: + encryption: + description: + - 0 Specifies an UNENCRYPTED authentication key will follow. + - 3 Specifies an 3DES ENCRYPTED authentication key will follow. + - 7 Specifies a Cisco type 7 ENCRYPTED authentication key will follow. + type: int + key: + description: + - Authentication key. + - Valid values are Cisco type 7 ENCRYPTED password, 3DES ENCRYPTED password + and UNENCRYPTED (cleartext) password based on the value of encryption key. + type: str + required: True + message_digest_key: + description: + - Message digest authentication password (key) settings. + type: dict + suboptions: + key_id: + description: + - Key ID. + type: int + required: True + encryption: + description: + - 0 Specifies an UNENCRYPTED ospf password (key) will follow. + - 3 Specifies an 3DES ENCRYPTED ospf password (key) will follow. + - 7 Specifies a Cisco type 7 ENCRYPTED the ospf password (key) will follow. + type: int + key: + description: + - Authentication key. + - Valid values are Cisco type 7 ENCRYPTED password, 3DES ENCRYPTED password + and UNENCRYPTED (cleartext) password based on the value of encryption key. + type: str + required: True + cost: + description: + - Cost associated with interface. + type: int + dead_interval: + description: + - Dead interval value (in seconds). + type: int + hello_interval: + description: + - Hello interval value (in seconds). + type: int + instance: + description: + - Instance identifier. + type: int + mtu_ignore: + description: + - Enable/disable OSPF MTU mismatch detection. + type: bool + network: + description: + - Network type. + type: str + choices: ["broadcast", "point-to-point"] + passive_interface: + description: + - Suppress routing updates on the interface. + - This option is mutually exclusive with I(default_passive_interface). + type: bool + default_passive_interface: + description: + - Set passive-interface attribute on this interface to default. + - This option is mutually exclusive with I(passive_interface). + type: bool + priority: + description: + - Router priority. + type: int + retransmit_interval: + description: + - Packet retransmission interval. + type: int + shutdown: + description: + - Shutdown OSPF on this interface. + type: bool + transmit_delay: + description: + - Packet transmission delay. + type: int + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# interface Ethernet1/2 +# no switchport +# interface Ethernet1/3 +# no switchport + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_ospf_interfaces: + config: + - name: Ethernet1/1 + address_family: + - afi: ipv4 + processes: + - process_id: "100" + area: + area_id: 1.1.1.1 + secondaries: False + multi_areas: + - 11.11.11.11 + - afi: ipv6 + processes: + - process_id: "200" + area: + area_id: 2.2.2.2 + multi_areas: + - 21.0.0.0 + - process_id: "300" + multi_areas: + - 50.50.50.50 + multi_areas: + - 16.10.10.10 + - name: Ethernet1/2 + address_family: + - afi: ipv4 + authentication: + enable: True + key_chain: test-1 + message_digest_key: + key_id: 10 + encryption: 3 + key: abc01d272be25d29 + cost: 100 + - afi: ipv6 + network: broadcast + shutdown: True + - name: Ethernet1/3 + address_family: + - afi: ipv4 + authentication_key: + encryption: 7 + key: 12090404011C03162E + state: merged + +# Task output +# ------------- +# "before": [ +# { +# "name": "Ethernet1/1" +# }, +# { +# "name": "Ethernet1/2" +# }, +# { +# "name": "Ethernet1/3" +# }, +# ] +# +# "commands": [ +# "interface Ethernet1/1", +# "ip router ospf multi-area 11.11.11.11", +# "ip router ospf 100 area 1.1.1.1 secondaries none", +# "ipv6 router ospfv3 multi-area 16.10.10.10", +# "ipv6 router ospfv3 200 area 2.2.2.2", +# "ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "ipv6 router ospfv3 300 multi-area 50.50.50.50", +# "interface Ethernet1/2", +# "ip ospf authentication key-chain test-1", +# "ip ospf authentication", +# "ip ospf message-digest-key 10 md5 3 abc01d272be25d29", +# "ip ospf cost 100", +# "ospfv3 network broadcast", +# "ospfv3 shutdown", +# "interface Ethernet1/3", +# "ip ospf authentication-key 7 12090404011C03162E" +# ] +# +# "after": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] + +# After state: +# ------------- +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + + +# Using replaced + +# Before state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + +- name: Replace OSPF configurations of listed interfaces with provided configurations + cisco.nxos.nxos_ospf_interfaces: + config: + - name: Ethernet1/1 + address_family: + - afi: ipv4 + processes: + - process_id: "100" + area: + area_id: 1.1.1.1 + secondaries: False + multi_areas: + - 11.11.11.12 + - name: Ethernet1/3 + state: replaced + +# Task output +# ------------- +# "before": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] +# +# "commands": [ +# "interface Ethernet1/1", +# "ip router ospf multi-area 11.11.11.12", +# "no ip router ospf multi-area 11.11.11.11", +# "no ipv6 router ospfv3 multi-area 16.10.10.10", +# "no ipv6 router ospfv3 200 area 2.2.2.2", +# "no ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "no ipv6 router ospfv3 300 multi-area 50.50.50.50", +# "interface Ethernet1/3", +# "no ip ospf authentication-key 7 12090404011C03162E" +# ] +# +# "after": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.12" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "name": "Ethernet1/3" +# }, +# +# After state: +# ------------- +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.12 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport + + +# Using overridden + +# Before state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + +- name: Override all OSPF interfaces configuration with provided configuration + cisco.nxos.nxos_ospf_interfaces: + config: + - name: Ethernet1/1 + address_family: + - afi: ipv4 + processes: + - process_id: "100" + area: + area_id: 1.1.1.1 + secondaries: False + multi_areas: + - 11.11.11.12 + state: overridden + +# Task output +# ------------- +# "before": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] +# +# "commands": [ +# "interface Ethernet1/2", +# "no ip ospf authentication key-chain test-1", +# "no ip ospf authentication", +# "no ip ospf message-digest-key 10 md5 3 abc01d272be25d29", +# "no ip ospf cost 100", +# "no ospfv3 network broadcast", +# "no ospfv3 shutdown", +# "interface Ethernet1/3", +# "no ip ospf authentication-key 7 12090404011C03162E", +# "interface Ethernet1/1", +# "ip router ospf multi-area 11.11.11.12", +# "no ip router ospf multi-area 11.11.11.11", +# "no ipv6 router ospfv3 multi-area 16.10.10.10", +# "no ipv6 router ospfv3 200 area 2.2.2.2", +# "no ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "no ipv6 router ospfv3 300 multi-area 50.50.50.50" +# ] +# +# "after": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.12" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "name": "Ethernet1/2" +# }, +# { +# "name": "Ethernet1/3" +# }, +# ] + +# After state: +# ------------- +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.12 +# interface Ethernet1/2 +# no switchport +# interface Ethernet1/3 +# no switchport + +# Using deleted to delete OSPF config of a single interface + +# Before state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + +- name: Delete OSPF config from a single interface + cisco.nxos.nxos_ospf_interfaces: + config: + - name: Ethernet1/1 + state: deleted + +# Task output +# ------------- +# "before": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] +# +# "commands": [ +# "interface Ethernet1/1", +# "no ip router ospf multi-area 11.11.11.11", +# "no ip router ospf 100 area 1.1.1.1 secondaries none", +# "no ipv6 router ospfv3 multi-area 16.10.10.10", +# "no ipv6 router ospfv3 200 area 2.2.2.2", +# "no ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "no ipv6 router ospfv3 300 multi-area 50.50.50.50" +# ] +# +# "before": [ +# { +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] + +# After state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + +# Using deleted to delete OSPF config from all interfaces + +# Before state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport +# ip ospf authentication-key 7 12090404011C03162E + +- name: Delete OSPF config from all interfaces + cisco.nxos.nxos_ospf_interfaces: + state: deleted + +# Task output +# ------------- +# "before": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] +# +# "commands": [ +# "interface Ethernet1/1", +# "no ip router ospf multi-area 11.11.11.11", +# "no ip router ospf 100 area 1.1.1.1 secondaries none", +# "no ipv6 router ospfv3 multi-area 16.10.10.10", +# "no ipv6 router ospfv3 200 area 2.2.2.2", +# "no ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "no ipv6 router ospfv3 300 multi-area 50.50.50.50", +# "interface Ethernet1/2", +# "no ip ospf authentication key-chain test-1", +# "no ip ospf authentication", +# "no ip ospf message-digest-key 10 md5 3 abc01d272be25d29", +# "no ip ospf cost 100", +# "no ospfv3 network broadcast", +# "no ospfv3 shutdown", +# "interface Ethernet1/3", +# "no ip ospf authentication-key 7 12090404011C03162E" +# ] +# +# "after": [ +# { +# "name": "Ethernet1/1" +# }, +# { +# "name": "Ethernet1/2" +# }, +# { +# "name": "Ethernet1/3" +# }, +# ] + +# After state: +# ------------ +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# interface Ethernet1/2 +# no switchport +# interface Ethernet1/3 +# no switchport + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_ospf_interfaces: + config: + - name: Ethernet1/1 + address_family: + - afi: ipv4 + processes: + - process_id: "100" + area: + area_id: 1.1.1.1 + secondaries: False + multi_areas: + - 11.11.11.11 + - afi: ipv6 + processes: + - process_id: "200" + area: + area_id: 2.2.2.2 + multi_areas: + - 21.0.0.0 + - process_id: "300" + multi_areas: + - 50.50.50.50 + multi_areas: + - 16.10.10.10 + - name: Ethernet1/2 + address_family: + - afi: ipv4 + authentication: + enable: True + key_chain: test-1 + message_digest_key: + key_id: 10 + encryption: 3 + key: abc01d272be25d29 + cost: 100 + - afi: ipv6 + network: broadcast + shutdown: True + - name: Ethernet1/3 + address_family: + - afi: ipv4 + authentication_key: + encryption: 7 + key: 12090404011C03162E + state: rendered + +# Task Output (redacted) +# ----------------------- +# "rendered": [ +# "interface Ethernet1/1", +# "ip router ospf multi-area 11.11.11.11", +# "ip router ospf 100 area 1.1.1.1 secondaries none", +# "ipv6 router ospfv3 multi-area 16.10.10.10", +# "ipv6 router ospfv3 200 area 2.2.2.2", +# "ipv6 router ospfv3 200 multi-area 21.0.0.0", +# "ipv6 router ospfv3 300 multi-area 50.50.50.50", +# "interface Ethernet1/2", +# "ip ospf authentication key-chain test-1", +# "ip ospf authentication", +# "ip ospf message-digest-key 10 md5 3 abc01d272be25d29", +# "ip ospf cost 100", +# "ospfv3 network broadcast", +# "ospfv3 shutdown", +# "interface Ethernet1/3", +# "ip ospf authentication-key 7 12090404011C03162E" +# ] + +# Using parsed + +# parsed.cfg +# ------------ +# interface Ethernet1/1 +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.11 +# ipv6 router ospfv3 200 area 2.2.2.2 +# ipv6 router ospfv3 200 multi-area 21.0.0.0 +# ipv6 router ospfv3 300 multi-area 50.50.50.50 +# ipv6 router ospfv3 multi-area 16.10.10.10 +# interface Ethernet1/2 +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# ip ospf authentication-key 7 12090404011C03162E + +- name: arse externally provided OSPF interfaces config + cisco.nxos.nxos_ospf_interfaces: + running_config: "{{ lookup('file', 'ospf_interfaces.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# "parsed": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.11" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# }, +# { +# "afi": "ipv6", +# "multi_areas": [ +# "16.10.10.10" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "2.2.2.2" +# }, +# "multi_areas": [ +# "21.0.0.0" +# ], +# "process_id": "200" +# }, +# { +# "multi_areas": [ +# "50.50.50.50" +# ], +# "process_id": "300" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication_key": { +# "encryption": 7, +# "key": "12090404011C03162E" +# } +# } +# ], +# "name": "Ethernet1/3" +# }, +# ] + +# Using gathered + +# On-box config + +# NXOS# show running-config | section ^interface +# interface Ethernet1/1 +# no switchport +# ip router ospf 100 area 1.1.1.1 secondaries none +# ip router ospf multi-area 11.11.11.12 +# interface Ethernet1/2 +# no switchport +# ip ospf authentication +# ip ospf authentication key-chain test-1 +# ip ospf message-digest-key 10 md5 3 abc01d272be25d29 +# ip ospf cost 100 +# ospfv3 network broadcast +# ospfv3 shutdown +# interface Ethernet1/3 +# no switchport + +# Task output (redacted) +# ----------------------- +# "gathered": [ +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "multi_areas": [ +# "11.11.11.12" +# ], +# "processes": [ +# { +# "area": { +# "area_id": "1.1.1.1", +# "secondaries": false +# }, +# "process_id": "100" +# } +# ] +# } +# ], +# "name": "Ethernet1/1" +# }, +# { +# "address_family": [ +# { +# "afi": "ipv4", +# "authentication": { +# "enable": true, +# "key_chain": "test-1" +# }, +# "cost": 100, +# "message_digest_key": { +# "encryption": 3, +# "key": "abc01d272be25d29", +# "key_id": 10 +# } +# }, +# { +# "afi": "ipv6", +# "network": "broadcast", +# "shutdown": true +# } +# ], +# "name": "Ethernet1/2" +# }, +# { +# "name": "Ethernet1/3" +# }, +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - interface Ethernet1/1 + - ip router ospf multi-area 11.11.11.11 + - ip router ospf 100 area 1.1.1.1 secondaries none + - no ipv6 router ospfv3 multi-area 16.10.10.10 + - ipv6 router ospfv3 200 area 2.2.2.2 + - ipv6 router ospfv3 200 multi-area 21.0.0.0 + - ipv6 router ospfv3 300 multi-area 50.50.50.50 + - interface Ethernet1/2 + - no ip ospf authentication key-chain test-1 + - ip ospf authentication +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.ospf_interfaces.ospf_interfaces import ( + Ospf_interfacesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.ospf_interfaces.ospf_interfaces import ( + Ospf_interfaces, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ospf_interfacesArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Ospf_interfaces(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv2.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv2.py new file mode 100644 index 00000000..174640a1 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv2.py @@ -0,0 +1,1984 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_ospfv2 +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_ospfv2 +short_description: OSPFv2 resource module +description: +- This module manages OSPFv2 configuration on devices running Cisco NX-OS. +version_added: 1.0.0 +notes: +- Tested against NX-OS 7.0(3)I5(1). +- Unsupported for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section "^router ospf .*"). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of OSPF process configuration. + type: dict + suboptions: + processes: + description: + - A list of OSPF instances' configurations. + type: list + elements: dict + suboptions: + areas: + description: + - Configure properties of OSPF Areas. + type: list + elements: dict + suboptions: + area_id: + description: + - The Area ID in IP Address format. + type: str + required: true + authentication: + description: + - Authentication settings for the Area. + type: dict + suboptions: + set: + description: + - Set authentication for the area. + type: bool + message_digest: + description: + - Use message-digest authentication. + type: bool + default_cost: + description: + - Specify the default cost for default summary LSA. + type: int + filter_list: + description: + - Filter prefixes between OSPF areas. + type: list + elements: dict + suboptions: + route_map: + description: + - The Route-map name. + type: str + required: true + direction: + description: + - The direction to apply the route map. + type: str + choices: [in, out] + required: true + nssa: + description: + - NSSA settings for the area. + type: dict + suboptions: + set: + description: + - Configure area as NSSA. + type: bool + default_information_originate: + description: + - Originate Type-7 default LSA into NSSA area. + type: bool + no_redistribution: + description: + - Do not send redistributed LSAs into NSSA area. + type: bool + no_summary: + description: + - Do not send summary LSAs into NSSA area. + type: bool + translate: + description: + - Translate LSA. + type: dict + suboptions: + type7: + description: + - Translate from Type 7 to Type 5. + type: dict + suboptions: + always: + description: + - Always translate LSAs + type: bool + never: + description: + - Never translate LSAs + type: bool + supress_fa: + description: + - Suppress forwarding address in translated LSAs. + type: bool + ranges: + description: + - Configure an address range for the area. + type: list + elements: dict + suboptions: + prefix: + description: + - IP in Prefix format (x.x.x.x/len) + type: str + required: true + cost: + description: + - Cost to use for the range. + type: int + not_advertise: + description: + - Suppress advertising the specified range. + type: bool + stub: + description: + - Settings for configuring the area as a stub. + type: dict + suboptions: + set: + description: + - Configure the area as a stub. + type: bool + no_summary: + description: + - Prevent ABR from sending summary LSAs into stub area. + type: bool + auto_cost: + description: + - Calculate OSPF cost according to bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: + - Reference bandwidth used to assign OSPF cost. + type: int + required: true + unit: + description: + - Specify in which unit the reference bandwidth is specified. + type: str + required: true + choices: [Gbps, Mbps] + bfd: + description: + - Enable BFD on all OSPF interfaces. + type: bool + default_information: + description: + - Control distribution of default routes. + type: dict + suboptions: + originate: + description: + - Distribute a default route. + type: dict + suboptions: + set: + description: + - Enable distribution of default route. + type: bool + always: + description: + - Always advertise a default route. + type: bool + route_map: + description: + - Policy to control distribution of default routes + type: str + default_metric: + description: + - Specify default metric for redistributed routes. + type: int + distance: + description: + - Configure the OSPF administrative distance. + type: int + flush_routes: + description: + - Flush routes on a non-graceful controlled restart. + type: bool + graceful_restart: + description: + - Configure graceful restart. + type: dict + suboptions: + set: + description: + - Enable graceful-restart. + type: bool + grace_period: + description: + - Configure maximum interval to restart gracefully. + type: int + helper_disable: + description: + - Enable/Disable helper mode. + type: bool + isolate: + description: + - Isolate this router from OSPF perspective. + type: bool + log_adjacency_changes: + description: + - Log changes in adjacency state. + type: dict + suboptions: + log: + description: + - Enable/disable logging changes in adjacency state. + type: bool + detail: + description: + - Notify all state changes. + type: bool + max_lsa: + description: + - Feature to limit the number of non-self-originated LSAs. + type: dict + suboptions: + max_non_self_generated_lsa: + description: + - Set the maximum number of non self-generated LSAs. + type: int + required: true + threshold: + description: + - Threshold value (%) at which to generate a warning message. + type: int + ignore_count: + description: + - Set count on how many times adjacencies can be suppressed. + type: int + ignore_time: + description: + - Set time during which all adjacencies are suppressed. + type: int + reset_time: + description: + - Set number of minutes after which ignore-count is reset to zero. + type: int + warning_only: + description: + - Log a warning message when limit is exceeded. + type: bool + max_metric: + description: + - Maximize the cost metric. + type: dict + suboptions: + router_lsa: + description: + - Router LSA configuration. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: + - External LSA configuration. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + include_stub: + description: + - Advertise Max metric for Stub links as well. + type: bool + on_startup: + description: + - Effective only at startup. + type: dict + suboptions: + set: + description: + - Set on-startup attribute. + type: bool + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp_asn: + description: + - ASN of BGP to wait for. + type: int + summary_lsa: + description: + - Summary LSAs configuration. + type: dict + suboptions: + set: + description: + - Set summary-lsa attribute. + type: bool + max_metric_value: + description: + - Max metric value for summary LSAs. + type: int + maximum_paths: + description: + - Maximum paths per destination. + type: int + mpls: + description: + - OSPF MPLS configuration settings. + type: dict + suboptions: + traffic_eng: + description: + - OSPF MPLS Traffic Engineering commands. + type: dict + suboptions: + areas: + description: + - List of Area IDs. + type: list + elements: dict + suboptions: + area_id: + description: + - Area Id in ip address format. + type: str + multicast_intact: + description: + - MPLS TE multicast support. + type: bool + router_id: + description: + - Router ID associated with TE. + type: str + name_lookup: + description: + - Display OSPF router ids as DNS names. + type: bool + passive_interface: + description: + - Suppress routing updates on the interface. + type: dict + suboptions: + default: + description: + - Interfaces passive by default. + type: bool + process_id: + description: + - The OSPF process tag. + type: str + required: true + redistribute: + description: + - Redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - The name of the protocol. + type: str + choices: [bgp, direct, eigrp, isis, lisp, ospf, rip, static] + required: true + id: + description: + - The identifier for the protocol specified. + type: str + route_map: + description: + - The route map policy to constrain redistribution. + type: str + required: true + rfc1583compatibility: + description: + - Configure 1583 compatibility for external path preferences. + type: bool + router_id: + description: + - Set OSPF process router-id. + type: str + shutdown: + description: + - Shutdown the OSPF protocol instance. + type: bool + summary_address: + description: + - Configure route summarization for redistribution. + type: list + elements: dict + suboptions: + prefix: + description: + - IP prefix in format x.x.x.x/ml. + type: str + required: true + not_advertise: + description: + - Suppress advertising the specified summary. + type: bool + tag: + description: + - A 32-bit tag value. + type: int + table_map: + description: + - Policy for filtering/modifying OSPF routes before sending them to RIB. + type: dict + suboptions: + name: + description: + - The Route Map name. + type: str + required: true + filter: + description: + - Block the OSPF routes from being sent to RIB. + type: bool + timers: + description: + - Configure timer related constants. + type: dict + suboptions: + lsa_arrival: + description: + - Mimimum interval between arrival of a LSA. + type: int + lsa_group_pacing: + description: + - LSA group refresh/maxage interval. + type: int + throttle: + description: + - Configure throttle related constants. + type: dict + suboptions: + lsa: + description: + - Set rate-limiting for LSA generation. + type: dict + suboptions: + start_interval: + description: + - The start interval. + type: int + hold_interval: + description: + - The hold interval. + type: int + max_interval: + description: + - The max interval. + type: int + spf: + description: + - Set OSPF SPF timers. + type: dict + suboptions: + initial_spf_delay: + description: + - Initial SPF schedule delay in milliseconds. + type: int + min_hold_time: + description: + - Minimum hold time between SPF calculations. + type: int + max_wait_time: + description: + - Maximum wait time between SPF calculations. + type: int + vrfs: + description: + - Configure VRF specific OSPF settings. + type: list + elements: dict + suboptions: + areas: + description: + - Configure properties of OSPF Areas. + type: list + elements: dict + suboptions: + area_id: + description: + - The Area ID in IP Address format. + type: str + required: true + authentication: + description: + - Authentication settings for the Area. + type: dict + suboptions: + set: + description: + - Set authentication for the area. + type: bool + message_digest: + description: + - Use message-digest authentication. + type: bool + default_cost: + description: + - Specify the default cost for default summary LSA. + type: int + filter_list: + description: + - Filter prefixes between OSPF areas. + type: list + elements: dict + suboptions: + route_map: + description: + - The Route-map name. + type: str + required: true + direction: + description: + - The direction to apply the route map. + type: str + choices: [in, out] + required: true + nssa: + description: + - NSSA settings for the area. + type: dict + suboptions: + set: + description: + - Configure area as NSSA. + type: bool + default_information_originate: + description: + - Originate Type-7 default LSA into NSSA area. + type: bool + no_redistribution: + description: + - Do not send redistributed LSAs into NSSA area. + type: bool + no_summary: + description: + - Do not send summary LSAs into NSSA area. + type: bool + translate: + description: + - Translate LSA. + type: dict + suboptions: + type7: + description: + - Translate from Type 7 to Type 5. + type: dict + suboptions: + always: + description: + - Always translate LSAs + type: bool + never: + description: + - Never translate LSAs + type: bool + supress_fa: + description: + - Suppress forwarding address in translated LSAs. + type: bool + ranges: + description: + - Configure an address range for the area. + type: list + elements: dict + suboptions: + prefix: + description: + - IP in Prefix format (x.x.x.x/len) + type: str + required: true + cost: + description: + - Cost to use for the range. + type: int + not_advertise: + description: + - Suppress advertising the specified range. + type: bool + stub: + description: + - Settings for configuring the area as a stub. + type: dict + suboptions: + set: + description: + - Configure the area as a stub. + type: bool + no_summary: + description: + - Prevent ABR from sending summary LSAs into stub area. + type: bool + auto_cost: + description: + - Calculate OSPF cost according to bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: + - Reference bandwidth used to assign OSPF cost. + type: int + required: true + unit: + description: + - Specify in which unit the reference bandwidth is specified. + type: str + required: True + choices: [Gbps, Mbps] + bfd: + description: + - Enable BFD on all OSPF interfaces. + type: bool + default_information: + description: + - Control distribution of default routes. + type: dict + suboptions: + originate: + description: + - Distribute a default route. + type: dict + suboptions: + set: + description: + - Enable distribution of default route. + type: bool + always: + description: + - Always advertise a default route. + type: bool + route_map: + description: + - Policy to control distribution of default routes + type: str + default_metric: + description: + - Specify default metric for redistributed routes. + type: int + distance: + description: + - Configure the OSPF administrative distance. + type: int + down_bit_ignore: + description: + - Configure a PE router to ignore the DN bit for network summary, + external and NSSA external LSA. + type: bool + capability: + description: + - OSPF capability settings. + type: dict + suboptions: + vrf_lite: + description: + - Enable VRF-lite capability settings. + type: dict + suboptions: + set: + description: + - Enable VRF-lite support. + type: bool + evpn: + description: + - Ethernet VPN. + type: bool + graceful_restart: + description: + - Configure graceful restart. + type: dict + suboptions: + set: + description: + - Enable graceful-restart. + type: bool + grace_period: + description: + - Configure maximum interval to restart gracefully. + type: int + helper_disable: + description: + - Enable/Disable helper mode. + type: bool + log_adjacency_changes: + description: + - Log changes in adjacency state. + type: dict + suboptions: + log: + description: + - Enable/disable logging changes in adjacency state. + type: bool + detail: + description: + - Notify all state changes. + type: bool + max_lsa: + description: + - Feature to limit the number of non-self-originated LSAs. + type: dict + suboptions: + max_non_self_generated_lsa: + description: + - Set the maximum number of non self-generated LSAs. + type: int + required: true + threshold: + description: + - Threshold value (%) at which to generate a warning message. + type: int + ignore_count: + description: + - Set count on how many times adjacencies can be suppressed. + type: int + ignore_time: + description: + - Set time during which all adjacencies are suppressed. + type: int + reset_time: + description: + - Set number of minutes after which ignore-count is reset to zero. + type: int + warning_only: + description: + - Log a warning message when limit is exceeded. + type: bool + max_metric: + description: + - Maximize the cost metric. + type: dict + suboptions: + router_lsa: + description: + - Router LSA configuration. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: + - External LSA configuration. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + include_stub: + description: + - Advertise Max metric for Stub links as well. + type: bool + on_startup: + description: + - Effective only at startup. + type: dict + suboptions: + set: + description: + - Set on-startup attribute. + type: bool + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp_asn: + description: + - ASN of BGP to wait for. + type: int + summary_lsa: + description: + - Summary LSAs configuration. + type: dict + suboptions: + set: + description: + - Set summary-lsa attribute. + type: bool + max_metric_value: + description: + - Max metric value for summary LSAs. + type: int + maximum_paths: + description: + - Maximum paths per destination. + type: int + name_lookup: + description: + - Display OSPF router ids as DNS names. + type: bool + passive_interface: + description: + - Suppress routing updates on the interface. + type: dict + suboptions: + default: + description: + - Interfaces passive by default. + type: bool + redistribute: + description: + - Redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - The name of the protocol. + type: str + choices: [bgp, direct, eigrp, isis, lisp, ospf, rip, static] + required: true + id: + description: + - The identifier for the protocol specified. + type: str + route_map: + description: + - The route map policy to constrain redistribution. + type: str + required: true + rfc1583compatibility: + description: + - Configure 1583 compatibility for external path preferences. + type: bool + router_id: + description: + - Set OSPF process router-id. + type: str + shutdown: + description: + - Shutdown the OSPF protocol instance. + type: bool + summary_address: + description: + - Configure route summarization for redistribution. + type: list + elements: dict + suboptions: + prefix: + description: + - IP prefix in format x.x.x.x/ml. + type: str + required: true + not_advertise: + description: + - Suppress advertising the specified summary. + type: bool + tag: + description: + - A 32-bit tag value. + type: int + table_map: + description: + - Policy for filtering/modifying OSPF routes before sending them to + RIB. + type: dict + suboptions: + name: + description: + - The Route Map name. + type: str + required: true + filter: + description: + - Block the OSPF routes from being sent to RIB. + type: bool + timers: + description: + - Configure timer related constants. + type: dict + suboptions: + lsa_arrival: + description: + - Mimimum interval between arrival of a LSA. + type: int + lsa_group_pacing: + description: + - LSA group refresh/maxage interval. + type: int + throttle: + description: + - Configure throttle related constants. + type: dict + suboptions: + lsa: + description: + - Set rate-limiting for LSA generation. + type: dict + suboptions: + start_interval: + description: + - The start interval. + type: int + hold_interval: + description: + - The hold interval. + type: int + max_interval: + description: + - The max interval. + type: int + spf: + description: + - Set OSPF SPF timers. + type: dict + suboptions: + initial_spf_delay: + description: + - Initial SPF schedule delay in milliseconds. + type: int + min_hold_time: + description: + - Minimum hold time between SPF calculations. + type: int + max_wait_time: + description: + - Maximum wait time between SPF calculations. + type: int + vrf: + description: + - Name/Identifier of the VRF. + type: str + required: true + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_ospfv2: + config: + processes: + - process_id: 100 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_1 + direction: in + - route_map: rmap_2 + direction: out + ranges: + - prefix: 198.51.100.64/27 + not_advertise: true + - prefix: 198.51.100.96/27 + cost: 120 + - area_id: 0.0.0.101 + authentication: + message_digest: true + redistribute: + - protocol: eigrp + id: 120 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + redistribute: + - protocol: static + route_map: zone1-static-connect + summary_address: + - prefix: 198.51.100.128/27 + tag: 121 + - prefix: 198.51.100.160/27 + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: true + no_summary: true + - area_id: 0.0.0.103 + nssa: + no_summary: true + translate: + type7: + always: true + - vrf: zone2 + auto_cost: + reference_bandwidth: 45 + unit: Gbps + state: merged + +# Task output +# ------------- +# before: {} +# +# commands: +# - router ospf 102 +# - router-id 198.51.100.1 +# - redistribute eigrp 120 route-map rmap_1 +# - redistribute direct route-map ospf102-direct-connect +# - area 0.0.0.100 filter-list route-map rmap_1 in +# - area 0.0.0.100 filter-list route-map rmap_2 out +# - area 0.0.0.100 range 198.51.100.64/27 not-advertise +# - area 0.0.0.100 range 198.51.100.96/27 cost 120 +# - area 0.0.0.101 authentication message-digest +# - vrf zone1 +# - router-id 198.51.100.129 +# - summary-address 198.51.100.128/27 tag 121 +# - summary-address 198.51.100.160/27 +# - redistribute static route-map zone1-static-connect +# - area 0.0.0.102 nssa no-summary default-information-originate +# - area 0.0.0.103 nssa no-summary +# - area 0.0.0.103 nssa translate type7 always +# - vrf zone2 +# - auto-cost reference-bandwidth 45 Gbps +# - router ospf 100 +# - router-id 203.0.113.20 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# - cost: 120 +# prefix: 198.51.100.96/27 +# - area_id: 0.0.0.101 +# authentication: +# message_digest: true +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# redistribute: +# - protocol: static +# route_map: zone1-static-connect +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# area 0.0.0.100 range 198.51.100.96/27 cost 120 +# area 0.0.0.101 authentication message-digest +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# redistribute static route-map zone1-static-connect +# summary-address 198.51.100.128/27 tag 121 +# summary-address 198.51.100.160/27 +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# area 0.0.0.100 range 198.51.100.96/27 cost 120 +# area 0.0.0.101 authentication message-digest +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# redistribute static route-map zone1-static-connect +# summary-address 198.51.100.128/27 tag 121 +# summary-address 198.51.100.160/27 +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Replace device configurations of listed OSPF processes with provided configurations + cisco.nxos.nxos_ospfv2: + config: + processes: + - process_id: 102 + router_id: 198.51.100.1 + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_8 + direction: in + ranges: + - prefix: 198.51.100.64/27 + not_advertise: true + - area_id: 0.0.0.101 + stub: + no_summary: true + redistribute: + - protocol: eigrp + id: 130 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + redistribute: + - protocol: bgp + id: 65563 + route_map: zone1-bgp-connect + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: true + no_summary: true + state: replaced + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# - cost: 120 +# prefix: 198.51.100.96/27 +# - area_id: 0.0.0.101 +# authentication: +# message_digest: true +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# redistribute: +# - protocol: static +# route_map: zone1-static-connect +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - router ospf 102 +# - redistribute eigrp 130 route-map rmap_1 +# - no redistribute eigrp 120 route-map rmap_1 +# - area 0.0.0.100 filter-list route-map rmap_8 in +# - no area 0.0.0.100 filter-list route-map rmap_2 out +# - no area 0.0.0.100 range 198.51.100.96/27 +# - no area 0.0.0.101 authentication +# - area 0.0.0.101 stub no-summary +# - vrf zone1 +# - no summary-address 198.51.100.128/27 tag 121 +# - no summary-address 198.51.100.160/27 +# - redistribute bgp 65563 route-map zone1-bgp-connect +# - no redistribute static route-map zone1-static-connect +# - no area 0.0.0.103 nssa +# - no area 0.0.0.103 nssa translate type7 always +# - no vrf zone2 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.101 +# stub: +# no_summary: true +# - area_id: 0.0.0.100 +# filter_list: +# - direction: in +# route_map: rmap_8 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "130" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# redistribute: +# - id: "65563" +# protocol: bgp +# route_map: zone1-bgp-connect +# router_id: 198.51.100.129 +# vrf: zone1 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# area 0.0.0.101 stub no-summary +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 130 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_8 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# redistribute bgp 65563 route-map zone1-bgp-connect + +# Using overridden + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# area 0.0.0.100 range 198.51.100.96/27 cost 120 +# area 0.0.0.101 authentication message-digest +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# redistribute static route-map zone1-static-connect +# summary-address 198.51.100.128/27 tag 121 +# summary-address 198.51.100.160/27 +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Override all OSPF configuration with provided configuration + cisco.nxos.nxos_ospfv2: + config: + processes: + - process_id: 104 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + shutdown: true + state: overridden + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# - cost: 120 +# prefix: 198.51.100.96/27 +# - area_id: 0.0.0.101 +# authentication: +# message_digest: true +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# redistribute: +# - protocol: static +# route_map: zone1-static-connect +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospf 100 +# - router ospf 104 +# - router-id 203.0.113.20 +# - router ospf 102 +# - shutdown +# - no redistribute direct route-map ospf102-direct-connect +# - no redistribute eigrp 120 route-map rmap_1 +# - no area 0.0.0.100 filter-list route-map rmap_2 out +# - no area 0.0.0.100 filter-list route-map rmap_1 in +# - no area 0.0.0.100 range 198.51.100.64/27 +# - no area 0.0.0.100 range 198.51.100.96/27 +# - no area 0.0.0.101 authentication +# - no vrf zone1 +# - no vrf zone2 +# +# after: +# processes: +# - process_id: "102" +# router_id: 198.51.100.1 +# shutdown: true +# - process_id: "104" +# router_id: 203.0.113.20 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 102 +# router-id 198.51.100.1 +# shutdown +# router ospf 104 +# router-id 203.0.113.20 + +# Using deleted to delete a single OSPF process + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# area 0.0.0.100 range 198.51.100.96/27 cost 120 +# area 0.0.0.101 authentication message-digest +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# redistribute static route-map zone1-static-connect +# summary-address 198.51.100.128/27 tag 121 +# summary-address 198.51.100.160/27 +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Delete a single OSPF process + cisco.nxos.nxos_ospfv2: + config: + processes: + - process_id: 102 + state: deleted + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# - cost: 120 +# prefix: 198.51.100.96/27 +# - area_id: 0.0.0.101 +# authentication: +# message_digest: true +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# redistribute: +# - protocol: static +# route_map: zone1-static-connect +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospf 102 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 + +# Using deleted all OSPF processes from the device + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospf 100 +# router-id 203.0.113.20 +# router ospf 102 +# router-id 198.51.100.1 +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# area 0.0.0.100 range 198.51.100.96/27 cost 120 +# area 0.0.0.101 authentication message-digest +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# redistribute static route-map zone1-static-connect +# summary-address 198.51.100.128/27 tag 121 +# summary-address 198.51.100.160/27 +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Delete all OSPF processes from the device + cisco.nxos.nxos_ospfv2: + state: deleted + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# - cost: 120 +# prefix: 198.51.100.96/27 +# - area_id: 0.0.0.101 +# authentication: +# message_digest: true +# process_id: "102" +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# redistribute: +# - protocol: static +# route_map: zone1-static-connect +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospf 100 +# - no router ospf 102 +# +# after: {} + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# nxos-9k-rdo# + +# Using rendered + +- name: Render platform specific configuration lines (without connecting to the device) + cisco.nxos.nxos_ospfv2: + config: + processes: + - process_id: 100 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_1 + direction: in + - route_map: rmap_2 + direction: out + ranges: + - prefix: 198.51.100.64/27 + not_advertise: true + - prefix: 198.51.100.96/27 + cost: 120 + - area_id: 0.0.0.101 + authentication: + message_digest: true + redistribute: + - protocol: eigrp + id: 120 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + redistribute: + - protocol: static + route_map: zone1-static-connect + summary_address: + - prefix: 198.51.100.128/27 + tag: 121 + - prefix: 198.51.100.160/27 + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: true + no_summary: true + - area_id: 0.0.0.103 + nssa: + no_summary: true + translate: + type7: + always: true + - vrf: zone2 + auto_cost: + reference_bandwidth: 45 + unit: Gbps + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - router ospf 100 +# - router-id 203.0.113.20 +# - router ospf 102 +# - router-id 198.51.100.1 +# - redistribute eigrp 120 route-map rmap_1 +# - redistribute direct route-map ospf102-direct-connect +# - area 0.0.0.100 filter-list route-map rmap_1 in +# - area 0.0.0.100 filter-list route-map rmap_2 out +# - area 0.0.0.100 range 198.51.100.64/27 not-advertise +# - area 0.0.0.100 range 198.51.100.96/27 cost 120 +# - area 0.0.0.101 authentication message-digest +# - vrf zone1 +# - router-id 198.51.100.129 +# - summary-address 198.51.100.128/27 tag 121 +# - summary-address 198.51.100.160/27 +# - redistribute static route-map zone1-static-connect +# - area 0.0.0.102 nssa no-summary default-information-originate +# - area 0.0.0.103 nssa no-summary +# - area 0.0.0.103 nssa translate type7 always +# - vrf zone2 +# - auto-cost reference-bandwidth 45 Gbps + +# Using parsed + +# parsed.cfg +# ------------ +# router ospf 100 +# router-id 192.0.100.1 +# area 0.0.0.101 nssa no-summary no-redistribution +# area 0.0.0.102 stub no-summary +# redistribute direct route-map ospf-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 192.0.2.0/24 not-advertise +# area 0.0.0.100 range 192.0.3.0/24 cost 120 +# area 0.0.0.100 authentication message-digest +# vrf zone1 +# router-id 192.0.100.2 +# area 0.0.100.1 nssa no-summary no-redistribution +# redistribute static route-map zone1-direct-connect +# summary-address 10.0.0.0/24 tag 120 +# summary-address 11.0.0.0/24 not-advertise +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps +# down-bit-ignore +# capability vrf-lite evpn +# shutdown +# router ospf 102 +# router-id 198.54.100.1 +# shutdown +# vrf zone2 +# summary-address 192.0.8.0/24 tag 120 +# vrf zone4 +# shutdown + +- name: Parse externally provided OSPFv2 config + cisco.nxos.nxos_ospfv2: + running_config: "{{ lookup('file', 'ospfv2.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# processes: +# - process_id: "100" +# areas: +# - area_id: 0.0.0.101 +# nssa: +# no_redistribution: true +# no_summary: true +# - area_id: 0.0.0.102 +# stub: +# no_summary: true +# - area_id: 0.0.0.100 +# authentication: +# message_digest: true +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 192.0.2.0/24 +# - cost: 120 +# prefix: 192.0.3.0/24 +# redistribute: +# - protocol: direct +# route_map: ospf-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 192.0.100.1 +# vrfs: +# - vrf: zone1 +# areas: +# - area_id: 0.0.100.1 +# nssa: +# no_redistribution: true +# no_summary: true +# redistribute: +# - protocol: static +# route_map: zone1-direct-connect +# router_id: 192.0.100.2 +# summary_address: +# - prefix: 10.0.0.0/24 +# tag: 120 +# - not_advertise: true +# prefix: 11.0.0.0/24 +# - vrf: zone2 +# auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# capability: +# vrf_lite: +# evpn: true +# down_bit_ignore: true +# shutdown: true +# - process_id: "102" +# router_id: 198.54.100.1 +# shutdown: true +# vrfs: +# - vrf: zone2 +# summary_address: +# - prefix: 192.0.8.0/24 +# tag: 120 +# - vrf: zone4 +# shutdown: true + +# Using gathered + +- name: Gather OSPFv2 facts using gathered + cisco.nxos.nxos_ospfv2: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# processes: +# - process_id: "102" +# areas: +# - area_id: 0.0.0.101 +# stub: +# no_summary: true +# - area_id: 0.0.0.100 +# filter_list: +# - direction: in +# route_map: rmap_8 +# ranges: +# - not_advertise: true +# prefix: 198.51.100.64/27 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "130" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 198.51.100.1 +# vrfs: +# - vrf: zone1 +# areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# redistribute: +# - id: "65563" +# protocol: bgp +# route_map: zone1-bgp-connect +# router_id: 198.51.100.129 +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - "router ospf 102" + - "router-id 198.54.100.1" + - "router ospf 100" + - "router-id 192.0.100.1" + - "redistribute eigrp 120 route-map rmap_1" + - "redistribute direct route-map ospf-direct-connect" + - "area 0.0.0.100 filter-list route-map rmap_1 in" + - "area 0.0.0.100 filter-list route-map rmap_2 out" + - "area 0.0.0.100 range 192.0.2.0/24 not-advertise" + - "area 0.0.0.100 range 192.0.3.0/24 cost 120" + - "vrf zone1" + - "router-id 192.0.100.2" + - "summary-address 10.0.0.0/24 tag 121" + - "summary-address 11.0.0.0/24" + - "redistribute static route-map zone1-direct-connect" + - "vrf zone2" + - "auto-cost reference-bandwidth 45 Gbps" + - "capability vrf-lite evpn" +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.ospfv2.ospfv2 import ( + Ospfv2Args, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.ospfv2.ospfv2 import ( + Ospfv2, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + required_if = [ + ("state", "merged", ("config",)), + ("state", "replaced", ("config",)), + ("state", "overridden", ("config",)), + ("state", "rendered", ("config",)), + ("state", "parsed", ("running_config",)), + ] + module = AnsibleModule( + argument_spec=Ospfv2Args.argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + result = Ospfv2(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv3.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv3.py new file mode 100644 index 00000000..27d9ff65 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ospfv3.py @@ -0,0 +1,1702 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_ospfv3 +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_ospfv3 +short_description: OSPFv3 resource module +description: +- This module manages OSPFv3 configuration on devices running Cisco NX-OS. +version_added: 1.2.0 +notes: +- Tested against NX-OS 7.0(3)I5(1). +- Unsupported for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section "^router ospfv3"). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of OSPFv3 process configuration. + type: dict + suboptions: + processes: + description: + - A list of OSPFv3 instances' configurations. + type: list + elements: dict + suboptions: + address_family: + description: + - IPv6 unicast address-family OSPFv3 settings. + type: dict + suboptions: + afi: + description: + - Configure OSPFv3 settings under IPv6 address-family. + type: str + choices: ['ipv6'] + safi: + description: + - Configure OSPFv3 settings under IPv6 unicast address-family. + type: str + choices: ['unicast'] + areas: + description: + - Configure properties of OSPF Areas under address-family. + type: list + elements: dict + suboptions: + area_id: + description: + - The Area ID in IP Address format. + type: str + required: True + default_cost: + description: + - Specify the default cost. + type: int + filter_list: + description: + - Filter prefixes between OSPF areas. + type: list + elements: dict + suboptions: + route_map: + description: + - The Route-map name. + type: str + required: True + direction: + description: + - The direction to apply the route map. + type: str + choices: [in, out] + required: True + ranges: + description: + - Configure an address range for the area. + type: list + elements: dict + suboptions: + prefix: + description: + - IP in Prefix format (x.x.x.x/len) + type: str + required: True + cost: + description: + - Cost to use for the range. + type: int + not_advertise: + description: + - Suppress advertising the specified range. + type: bool + default_information: + description: + - Control distribution of default routes. + type: dict + suboptions: + originate: + description: + - Distribute a default route. + type: dict + suboptions: + set: + description: + - Enable distribution of default route. + type: bool + always: + description: + - Always advertise a default route. + type: bool + route_map: + description: + - Policy to control distribution of default routes + type: str + distance: + description: + - Configure the OSPF administrative distance. + type: int + maximum_paths: + description: + - Maximum paths per destination. + type: int + redistribute: + description: + - Redistribute information from another routing protocol. + type: list + elements: dict + suboptions: + protocol: + description: + - The name of the protocol. + type: str + choices: [bgp, direct, eigrp, isis, lisp, ospfv3, rip, static] + required: True + id: + description: + - The identifier for the protocol specified. + type: str + route_map: + description: + - The route map policy to constrain redistribution. + type: str + required: True + summary_address: + description: + - Configure route summarization for redistribution. + type: list + elements: dict + suboptions: + prefix: + description: + - IPv6 prefix format 'xxxx:xxxx/ml', 'xxxx:xxxx::/ml' or 'xxxx::xx/128' + type: str + required: True + not_advertise: + description: + - Suppress advertising the specified summary. + type: bool + tag: + description: + - A 32-bit tag value. + type: int + table_map: + description: + - Policy for filtering/modifying OSPF routes before sending them to + RIB. + type: dict + suboptions: + name: + description: + - The Route Map name. + type: str + required: True + filter: + description: + - Block the OSPF routes from being sent to RIB. + type: bool + timers: + description: + - Configure timer related constants. + type: dict + suboptions: + throttle: + description: + - Configure throttle related constants. + type: dict + suboptions: + spf: + description: + - Set OSPF SPF timers. + type: dict + suboptions: + initial_spf_delay: + description: + - Initial SPF schedule delay in milliseconds. + type: int + min_hold_time: + description: + - Minimum hold time between SPF calculations. + type: int + max_wait_time: + description: + - Maximum wait time between SPF calculations. + type: int + areas: + description: + - Configure properties of OSPF Areas. + type: list + elements: dict + suboptions: + area_id: + description: + - The Area ID in IP Address format. + type: str + required: True + nssa: + description: + - NSSA settings for the area. + type: dict + suboptions: + set: + description: + - Configure area as NSSA. + type: bool + default_information_originate: + description: + - Originate Type-7 default LSA into NSSA area. + type: bool + no_redistribution: + description: + - Do not send redistributed LSAs into NSSA area. + type: bool + no_summary: + description: + - Do not send summary LSAs into NSSA area. + type: bool + route_map: + description: + - Policy to control distribution of default route. + type: str + translate: + description: + - Translate LSA. + type: dict + suboptions: + type7: + description: + - Translate from Type 7 to Type 5. + type: dict + suboptions: + always: + description: + - Always translate LSAs + type: bool + never: + description: + - Never translate LSAs + type: bool + supress_fa: + description: + - Suppress forwarding address in translated LSAs. + type: bool + stub: + description: + - Settings for configuring the area as a stub. + type: dict + suboptions: + set: + description: + - Configure the area as a stub. + type: bool + no_summary: + description: + - Prevent ABR from sending summary LSAs into stub area. + type: bool + auto_cost: + description: + - Calculate OSPF cost according to bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: + - Reference bandwidth used to assign OSPF cost. + type: int + required: True + unit: + description: + - Specify in which unit the reference bandwidth is specified. + type: str + required: True + choices: [Gbps, Mbps] + flush_routes: + description: + - Flush routes on a non-graceful controlled restart. + type: bool + graceful_restart: + description: + - Configure graceful restart. + type: dict + suboptions: + set: + description: + - Enable graceful-restart. + type: bool + grace_period: + description: + - Configure maximum interval to restart gracefully. + type: int + helper_disable: + description: + - Enable/Disable helper mode. + type: bool + planned_only: + description: + - Enable graceful restart only for a planned restart + type: bool + isolate: + description: + - Isolate this router from OSPF perspective. + type: bool + log_adjacency_changes: + description: + - Log changes in adjacency state. + type: dict + suboptions: + log: + description: + - Enable/disable logging changes in adjacency state. + type: bool + detail: + description: + - Notify all state changes. + type: bool + max_lsa: + description: + - Feature to limit the number of non-self-originated LSAs. + type: dict + suboptions: + max_non_self_generated_lsa: + description: + - Set the maximum number of non self-generated LSAs. + type: int + required: True + threshold: + description: + - Threshold value (%) at which to generate a warning message. + type: int + ignore_count: + description: + - Set count on how many times adjacencies can be suppressed. + type: int + ignore_time: + description: + - Set time during which all adjacencies are suppressed. + type: int + reset_time: + description: + - Set number of minutes after which ignore-count is reset to zero. + type: int + warning_only: + description: + - Log a warning message when limit is exceeded. + type: bool + max_metric: + description: + - Maximize the cost metric. + type: dict + suboptions: + router_lsa: + description: + - Router LSA configuration. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: + - External LSA configuration. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + stub_prefix_lsa: + description: + - Advertise Max metric for Stub links as well. + type: bool + on_startup: + description: + - Effective only at startup. + type: dict + suboptions: + set: + description: + - Set on-startup attribute. + type: bool + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp_asn: + description: + - ASN of BGP to wait for. + type: int + inter_area_prefix_lsa: + description: + - Inter-area-prefix LSAs configuration. + type: dict + suboptions: + set: + description: + - Set summary-lsa attribute. + type: bool + max_metric_value: + description: + - Max metric value for summary LSAs. + type: int + name_lookup: + description: + - Display OSPF router ids as DNS names. + type: bool + passive_interface: + description: + - Suppress routing updates on the interface. + type: dict + suboptions: + default: + description: + - Interfaces passive by default. + type: bool + process_id: + description: + - The OSPF process tag. + type: str + required: True + router_id: + description: + - Set OSPF process router-id. + type: str + shutdown: + description: + - Shutdown the OSPF protocol instance. + type: bool + timers: + description: + - Configure timer related constants. + type: dict + suboptions: + lsa_arrival: + description: + - Mimimum interval between arrival of a LSA. + type: int + lsa_group_pacing: + description: + - LSA group refresh/maxage interval. + type: int + throttle: + description: + - Configure throttle related constants. + type: dict + suboptions: + lsa: + description: + - Set rate-limiting for LSA generation. + type: dict + suboptions: + start_interval: + description: + - The start interval. + type: int + hold_interval: + description: + - The hold interval. + type: int + max_interval: + description: + - The max interval. + type: int + vrfs: + description: + - Configure VRF specific OSPF settings. + type: list + elements: dict + suboptions: + areas: + description: + - Configure properties of OSPF Areas. + type: list + elements: dict + suboptions: + area_id: + description: + - The Area ID in IP Address format. + type: str + required: True + nssa: + description: + - NSSA settings for the area. + type: dict + suboptions: + set: + description: + - Configure area as NSSA. + type: bool + default_information_originate: + description: + - Originate Type-7 default LSA into NSSA area. + type: bool + no_redistribution: + description: + - Do not send redistributed LSAs into NSSA area. + type: bool + no_summary: + description: + - Do not send summary LSAs into NSSA area. + type: bool + route_map: + description: + - Policy to control distribution of default route. + type: str + translate: + description: + - Translate LSA. + type: dict + suboptions: + type7: + description: + - Translate from Type 7 to Type 5. + type: dict + suboptions: + always: + description: + - Always translate LSAs + type: bool + never: + description: + - Never translate LSAs + type: bool + supress_fa: + description: + - Suppress forwarding address in translated LSAs. + type: bool + stub: + description: + - Settings for configuring the area as a stub. + type: dict + suboptions: + set: + description: + - Configure the area as a stub. + type: bool + no_summary: + description: + - Prevent ABR from sending summary LSAs into stub area. + type: bool + auto_cost: + description: + - Calculate OSPF cost according to bandwidth. + type: dict + suboptions: + reference_bandwidth: + description: + - Reference bandwidth used to assign OSPF cost. + type: int + required: True + unit: + description: + - Specify in which unit the reference bandwidth is specified. + type: str + required: True + choices: [Gbps, Mbps] + graceful_restart: + description: + - Configure graceful restart. + type: dict + suboptions: + set: + description: + - Enable graceful-restart. + type: bool + grace_period: + description: + - Configure maximum interval to restart gracefully. + type: int + helper_disable: + description: + - Enable/Disable helper mode. + type: bool + planned_only: + description: + - Enable graceful restart only for a planned restart + type: bool + log_adjacency_changes: + description: + - Log changes in adjacency state. + type: dict + suboptions: + log: + description: + - Enable/disable logging changes in adjacency state. + type: bool + detail: + description: + - Notify all state changes. + type: bool + max_lsa: + description: + - Feature to limit the number of non-self-originated LSAs. + type: dict + suboptions: + max_non_self_generated_lsa: + description: + - Set the maximum number of non self-generated LSAs. + type: int + required: True + threshold: + description: + - Threshold value (%) at which to generate a warning message. + type: int + ignore_count: + description: + - Set count on how many times adjacencies can be suppressed. + type: int + ignore_time: + description: + - Set time during which all adjacencies are suppressed. + type: int + reset_time: + description: + - Set number of minutes after which ignore-count is reset to zero. + type: int + warning_only: + description: + - Log a warning message when limit is exceeded. + type: bool + max_metric: + description: + - Maximize the cost metric. + type: dict + suboptions: + router_lsa: + description: + - Router LSA configuration. + type: dict + suboptions: + set: + description: + - Set router-lsa attribute. + type: bool + external_lsa: + description: + - External LSA configuration. + type: dict + suboptions: + set: + description: + - Set external-lsa attribute. + type: bool + max_metric_value: + description: + - Set max metric value for external LSAs. + type: int + stub_prefix_lsa: + description: + - Advertise Max metric for Stub links as well. + type: bool + on_startup: + description: + - Effective only at startup. + type: dict + suboptions: + set: + description: + - Set on-startup attribute. + type: bool + wait_period: + description: + - Wait period in seconds after startup. + type: int + wait_for_bgp_asn: + description: + - ASN of BGP to wait for. + type: int + inter_area_prefix_lsa: + description: + - Inter-area-prefix LSAs configuration. + type: dict + suboptions: + set: + description: + - Set summary-lsa attribute. + type: bool + max_metric_value: + description: + - Max metric value for summary LSAs. + type: int + name_lookup: + description: + - Display OSPF router ids as DNS names. + type: bool + passive_interface: + description: + - Suppress routing updates on the interface. + type: dict + suboptions: + default: + description: + - Interfaces passive by default. + type: bool + router_id: + description: + - Set OSPF process router-id. + type: str + shutdown: + description: + - Shutdown the OSPF protocol instance. + type: bool + timers: + description: + - Configure timer related constants. + type: dict + suboptions: + lsa_arrival: + description: + - Mimimum interval between arrival of a LSA. + type: int + lsa_group_pacing: + description: + - LSA group refresh/maxage interval. + type: int + throttle: + description: + - Configure throttle related constants. + type: dict + suboptions: + lsa: + description: + - Set rate-limiting for LSA generation. + type: dict + suboptions: + start_interval: + description: + - The start interval. + type: int + hold_interval: + description: + - The hold interval. + type: int + max_interval: + description: + - The max interval. + type: int + vrf: + description: + - Name/Identifier of the VRF. + type: str + required: True + state: + description: + - The state the configuration should be left in. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - parsed + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_ospfv3: + config: + processes: + - process_id: 100 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + address_family: + afi: ipv6 + safi: unicast + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_1 + direction: in + - route_map: rmap_2 + direction: out + ranges: + - prefix: 2001:db2::/32 + not_advertise: true + - prefix: 2001:db3::/32 + cost: 120 + redistribute: + - protocol: eigrp + id: 120 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: true + no_summary: true + - area_id: 0.0.0.103 + nssa: + no_summary: true + translate: + type7: + always: true + - vrf: zone2 + auto_cost: + reference_bandwidth: 45 + unit: Gbps + state: merged + +# Task output +# ------------- +# before: {} +# +# commands: +# - router ospf 102 +# - router-id 198.51.100.1 +# - address-family ipv6 unicast +# - redistribute eigrp 120 route-map rmap_1 +# - redistribute direct route-map ospf102-direct-connect +# - area 0.0.0.100 filter-list route-map rmap_1 in +# - area 0.0.0.100 filter-list route-map rmap_2 out +# - area 0.0.0.100 range 2001:db2::/32 not-advertise +# - area 0.0.0.100 range 2001:db3::/32 cost 120 +# - vrf zone1 +# - router-id 198.51.100.129 +# - area 0.0.0.102 nssa no-summary default-information-originate +# - area 0.0.0.103 nssa no-summary +# - area 0.0.0.103 nssa translate type7 always +# - vrf zone2 +# - auto-cost reference-bandwidth 45 Gbps +# - router ospf 100 +# - router-id 203.0.113.20 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family upv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Replace device configurations of listed OSPFv3 processes with provided configurations + cisco.nxos.nxos_ospfv3: + config: + processes: + - process_id: 102 + router_id: 198.51.100.1 + address_family: + afi: ipv6 + safi: unicast + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_8 + direction: in + ranges: + - not_advertise: true + prefix: 2001:db2::/32 + redistribute: + - protocol: eigrp + id: 130 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: True + no_summary: True + state: replaced + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - router ospf 102 +# - address-family ipv6 unicast +# - redistribute eigrp 130 route-map rmap_1 +# - no redistribute eigrp 120 route-map rmap_1 +# - area 0.0.0.100 filter-list route-map rmap_8 in +# - no area 0.0.0.100 filter-list route-map rmap_2 out +# - no area 0.0.0.100 range 2001:db3::/32 +# - vrf zone1 +# - no area 0.0.0.103 nssa +# - no area 0.0.0.103 nssa translate type7 always +# - no vrf zone2 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv6 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: in +# route_map: rmap_8 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "130" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# router_id: 198.51.100.129 +# vrf: zone1 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 130 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_8 in +# area 0.0.0.100 range 198.51.100.64/27 not-advertise +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate + +# Using overridden + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Override all OSPFv3 configuration with provided configuration + cisco.nxos.nxos_ospfv3: + config: + processes: + - process_id: 104 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + shutdown: true + state: overridden + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospfv3 100 +# - router ospfv3 104 +# - router-id 203.0.113.20 +# - router ospfv3 102 +# - shutdown +# - address-family ipv6 unicast +# - no redistribute direct route-map ospf102-direct-connect +# - no redistribute eigrp 120 route-map rmap_1 +# - no area 0.0.0.100 filter-list route-map rmap_2 out +# - no area 0.0.0.100 filter-list route-map rmap_1 in +# - no area 0.0.0.100 range 2001:db2::/32 +# - no area 0.0.0.100 range 2001:db3::/32 +# - no vrf zone1 +# - no vrf zone2 +# +# after: +# processes: +# - process_id: "102" +# router_id: 198.51.100.1 +# shutdown: true +# address_family: +# afi: ipv6 +# safi: unicast +# - process_id: "104" +# router_id: 203.0.113.20 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# shutdown +# router ospfv3 104 +# router-id 203.0.113.20 + +# Using deleted to delete a single OSPF process + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospf .*" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Delete a single OSPFv3 process + cisco.nxos.nxos_ospfv3: + config: + processes: + - process_id: 102 + state: deleted + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospfv3 102 +# +# after: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 + +# Using deleted all OSPFv3 processes from the device + +# Before state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# router ospfv3 100 +# router-id 203.0.113.20 +# router ospfv3 102 +# router-id 198.51.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf102-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.0.102 nssa no-summary default-information-originate +# area 0.0.0.103 nssa no-summary +# area 0.0.0.103 nssa translate type7 always +# vrf zone2 +# auto-cost reference-bandwidth 45 Gbps + +- name: Delete all OSPFv3 processes from the device + cisco.nxos.nxos_ospfv3: + state: deleted + +# Task output +# ------------- +# before: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +# commands: +# - no router ospfv3 100 +# - no router ospfv3 102 +# +# after: {} + +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^router ospfv3" +# nxos-9k-rdo# + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_ospfv3: + config: + processes: + - process_id: 100 + router_id: 203.0.113.20 + - process_id: 102 + router_id: 198.51.100.1 + address_family: + afi: ipv6 + safi: unicast + areas: + - area_id: 0.0.0.100 + filter_list: + - route_map: rmap_1 + direction: in + - route_map: rmap_2 + direction: out + ranges: + - prefix: 2001:db2::/32 + not_advertise: true + - prefix: 2001:db3::/32 + cost: 120 + redistribute: + - protocol: eigrp + id: 120 + route_map: rmap_1 + - protocol: direct + route_map: ospf102-direct-connect + vrfs: + - vrf: zone1 + router_id: 198.51.100.129 + areas: + - area_id: 0.0.0.102 + nssa: + default_information_originate: true + no_summary: true + - area_id: 0.0.0.103 + nssa: + no_summary: true + translate: + type7: + always: true + - vrf: zone2 + auto_cost: + reference_bandwidth: 45 + unit: Gbps + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - router ospfv3 100 +# - router-id 203.0.113.20 +# - router ospfv3 102 +# - router-id 198.51.100.1 +# - address-family ipv6 unicast +# - redistribute eigrp 120 route-map rmap_1 +# - redistribute direct route-map ospf102-direct-connect +# - area 0.0.0.100 filter-list route-map rmap_1 in +# - area 0.0.0.100 filter-list route-map rmap_2 out +# - area 0.0.0.100 range 2001:db2::/32 not-advertise +# - area 0.0.0.100 range 2001:db3::/32 cost 120 +# - vrf zone1 +# - router-id 198.51.100.129 +# - area 0.0.0.102 nssa no-summary default-information-originate +# - area 0.0.0.103 nssa no-summary +# - area 0.0.0.103 nssa translate type7 always +# - vrf zone2 +# - auto-cost reference-bandwidth 45 Gbps + +# Using parsed + +# parsed.cfg +# ------------ +# router ospfv3 100 +# router-id 192.0.100.1 +# address-family ipv6 unicast +# redistribute direct route-map ospf-direct-connect +# redistribute eigrp 120 route-map rmap_1 +# area 0.0.0.100 filter-list route-map rmap_2 out +# area 0.0.0.100 filter-list route-map rmap_1 in +# area 0.0.0.100 range 2001:db2::/32 not-advertise +# area 0.0.0.100 range 2001:db3::/32 cost 120 +# vrf zone1 +# router-id 198.51.100.129 +# area 0.0.100.1 nssa no-summary no-redistribution +# router ospfv3 102 +# router-id 198.54.100.1 +# shutdown + +- name: Parse externally provided OSPFv3 config + cisco.nxos.nxos_ospfv3: + running_config: "{{ lookup('file', 'ospfv2.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# processes: +# - process_id: "100" +# address_family: +# afi: ipv6 +# safi: unicast +# areas: +# - area_id: 0.0.0.101 +# nssa: +# no_redistribution: true +# no_summary: true +# - area_id: 0.0.0.102 +# stub: +# no_summary: true +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 192.0.2.0/24 +# - cost: 120 +# prefix: 192.0.3.0/24 +# redistribute: +# - protocol: direct +# route_map: ospf-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# router_id: 192.0.100.1 +# vrfs: +# - vrf: zone1 +# areas: +# - area_id: 0.0.100.1 +# nssa: +# no_redistribution: true +# no_summary: true +# router_id: 192.0.100.2 +# - process_id: "102" +# router_id: 198.54.100.1 +# shutdown: True + +# Using gathered + +- name: Gather OSPFv3 facts using gathered + cisco.nxos.nxos_ospfv3: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# processes: +# - process_id: "100" +# router_id: 203.0.113.20 +# - address_family: +# afi: ipv4 +# safi: unicast +# areas: +# - area_id: 0.0.0.100 +# filter_list: +# - direction: out +# route_map: rmap_2 +# - direction: in +# route_map: rmap_1 +# ranges: +# - not_advertise: true +# prefix: 2001:db2::/32 +# - cost: 120 +# prefix: 2001:db3::/32 +# redistribute: +# - protocol: direct +# route_map: ospf102-direct-connect +# - id: "120" +# protocol: eigrp +# route_map: rmap_1 +# process_id: "102" +# router_id: 198.51.100.1 +# vrfs: +# - areas: +# - area_id: 0.0.0.102 +# nssa: +# default_information_originate: true +# no_summary: true +# - area_id: 0.0.0.103 +# nssa: +# no_summary: true +# translate: +# type7: +# always: true +# router_id: 198.51.100.129 +# vrf: zone1 +# - auto_cost: +# reference_bandwidth: 45 +# unit: Gbps +# vrf: zone2 +# +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - "router ospfv3 102" + - "router-id 198.54.100.1" + - "router ospfv3 100" + - "router-id 192.0.100.1" + - "address-family ipv6 unicast" + - "redistribute eigrp 120 route-map rmap_1" + - "redistribute direct route-map ospf-direct-connect" + - "area 0.0.0.100 filter-list route-map rmap_1 in" + - "area 0.0.0.100 filter-list route-map rmap_2 out" + - "area 0.0.0.100 range 2001:db2::/32 not-advertise" + - "area 0.0.0.100 range 2001:db3::/32 cost 120" + - "vrf zone1" + - "router-id 192.0.100.2" + - "vrf zone2" + - "auto-cost reference-bandwidth 45 Gbps" +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.ospfv3.ospfv3 import ( + Ospfv3Args, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.ospfv3.ospfv3 import ( + Ospfv3, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Ospfv3Args.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Ospfv3(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_overlay_global.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_overlay_global.py new file mode 100644 index 00000000..70b25246 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_overlay_global.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_overlay_global +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Configures anycast gateway MAC of the switch. +description: +- Configures anycast gateway MAC of the switch. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Default restores params default value +- Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE", "EE:EE:EE:EE:EE:EE" + and "EEEE.EEEE.EEEE" +options: + anycast_gateway_mac: + description: + - Anycast gateway mac of the switch. + required: true + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_overlay_global: + anycast_gateway_mac: b.b.b +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["fabric forwarding anycast-gateway-mac 000B.000B.000B"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +PARAM_TO_COMMAND_KEYMAP = {"anycast_gateway_mac": "fabric forwarding anycast-gateway-mac"} + + +def get_existing(module, args): + existing = {} + config = str(get_config(module)) + + for arg in args: + command = PARAM_TO_COMMAND_KEYMAP[arg] + has_command = re.findall(r"(?:{0}\s)(?P<value>.*)$".format(command), config, re.M) + value = "" + if has_command: + value = has_command[0] + existing[arg] = value + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if value: + new_dict[new_key] = value + return new_dict + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, proposed in proposed_commands.items(): + existing_value = existing_commands.get(key) + if proposed == "default" and existing_value: + commands.append("no {0} {1}".format(key, existing_value)) + elif "anycast-gateway-mac" in key and proposed != "default": + proposed = normalize_mac(proposed, module) + existing_value = normalize_mac(existing_value, module) + if proposed != existing_value: + command = "{0} {1}".format(key, proposed) + commands.append(command) + if commands: + candidate.add(commands, parents=[]) + + +def normalize_mac(proposed_mac, module): + if proposed_mac is None: + return "" + try: + if "-" in proposed_mac: + splitted_mac = proposed_mac.split("-") + if len(splitted_mac) != 6: + raise ValueError + + for octect in splitted_mac: + if len(octect) != 2: + raise ValueError + + elif "." in proposed_mac: + splitted_mac = [] + splitted_dot_mac = proposed_mac.split(".") + if len(splitted_dot_mac) != 3: + raise ValueError + + for octect in splitted_dot_mac: + if len(octect) > 4: + raise ValueError + else: + octect_len = len(octect) + padding = 4 - octect_len + splitted_mac.append(octect.zfill(padding + 1)) + + elif ":" in proposed_mac: + splitted_mac = proposed_mac.split(":") + if len(splitted_mac) != 6: + raise ValueError + + for octect in splitted_mac: + if len(octect) != 2: + raise ValueError + else: + raise ValueError + except ValueError: + module.fail_json(msg="Invalid MAC address format", proposed_mac=proposed_mac) + + joined_mac = "".join(splitted_mac) + # fmt: off + mac = [joined_mac[i: i + 4] for i in range(0, len(joined_mac), 4)] + # fmt: on + return ".".join(mac).upper() + + +def main(): + argument_spec = dict(anycast_gateway_mac=dict(required=True, type="str")) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + result = {"changed": False, "commands": [], "warnings": warnings} + + args = PARAM_TO_COMMAND_KEYMAP.keys() + + existing = get_existing(module, args) + proposed = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + candidate = CustomNetworkConfig(indent=3) + get_commands(module, existing, proposed, candidate) + + if candidate: + candidate = candidate.items_text() + result["commands"] = candidate + + if not module.check_mode: + load_config(module, candidate) + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_pim.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim.py new file mode 100644 index 00000000..d9fc6f46 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_pim +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages configuration of a PIM instance. +description: +- Manages configuration of a Protocol Independent Multicast (PIM) instance. +notes: +- Unsupported for Cisco MDS +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +options: + bfd: + description: + - Enables BFD on all PIM interfaces. + - "Dependency: ''feature bfd''" + type: str + choices: + - enable + - disable + ssm_range: + description: + - Configure group ranges for Source Specific Multicast (SSM). Valid values are + multicast addresses or the keyword C(none) or keyword C(default). C(none) removes + all SSM group ranges. C(default) will set ssm_range to the default multicast + address. If you set multicast address, please ensure that it is not the same + as the C(default), otherwise use the C(default) option. + type: list + default: [] + elements: str +""" +EXAMPLES = """ +- name: Configure ssm_range, enable bfd + cisco.nxos.nxos_pim: + bfd: enable + ssm_range: 224.0.0.0/8 + +- name: Set to default + cisco.nxos.nxos_pim: + ssm_range: default + +- name: Remove all ssm group ranges + cisco.nxos.nxos_pim: + ssm_range: none +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - ip pim bfd + - ip pim ssm range 224.0.0.0/8 +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +PARAM_TO_COMMAND_KEYMAP = { + "bfd": "ip pim bfd", + "ssm_range": "ip pim ssm range", +} + + +def get_existing(module, args): + existing = {} + config = str(get_config(module)) + + for arg in args: + if "ssm_range" in arg: + # <value> may be 'n.n.n.n/s', 'none', or 'default' + m = re.search( + r"ssm range (?P<value>(?:[\s\d.\/]+|none|default))?$", + config, + re.M, + ) + if m: + # Remove rsvd SSM range + value = m.group("value").replace("232.0.0.0/8", "") + existing[arg] = value.split() + + elif "bfd" in arg and "ip pim bfd" in config: + existing[arg] = "enable" + + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if value is not None: + new_dict[new_key] = value + return new_dict + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + + for key, value in proposed_commands.items(): + command = "" + if key == "ip pim ssm range": + if value == "default": + # no cmd needs a value but the actual value does not matter + command = "no ip pim ssm range none" + elif value == "none": + command = "ip pim ssm range none" + elif value: + command = "ip pim ssm range {0}".format(value) + elif key == "ip pim bfd": + no_cmd = "no " if value == "disable" else "" + command = no_cmd + key + + if command: + commands.append(command) + + if commands: + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + bfd=dict(required=False, type="str", choices=["enable", "disable"]), + ssm_range=dict(required=False, type="list", default=[], elements="str"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + warnings = list() + result = {"changed": False, "commands": [], "warnings": warnings} + + params = module.params + args = [k for k in PARAM_TO_COMMAND_KEYMAP.keys() if params[k] is not None] + + # SSM syntax check + if "ssm_range" in args: + for item in params["ssm_range"]: + if re.search("none|default", item): + break + if len(item.split(".")) != 4: + module.fail_json( + msg="Valid ssm_range values are multicast addresses " + "or the keyword 'none' or the keyword 'default'.", + ) + + existing = get_existing(module, args) + proposed_args = dict((k, v) for k, v in params.items() if k in args) + + proposed = {} + for key, value in proposed_args.items(): + if key == "ssm_range": + if value and value[0] == "default": + if existing.get(key): + proposed[key] = "default" + else: + v = sorted(set([str(i) for i in value])) + ex = sorted(set([str(i) for i in existing.get(key, [])])) + if v != ex: + proposed[key] = " ".join(str(s) for s in v) + + elif key == "bfd": + if value != existing.get("bfd", "disable"): + proposed[key] = value + + elif value != existing.get(key): + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + get_commands(module, existing, proposed, candidate) + + if candidate: + candidate = candidate.items_text() + result["commands"] = candidate + result["changed"] = True + load_config(module, candidate) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_interface.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_interface.py new file mode 100644 index 00000000..4d0f1052 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_interface.py @@ -0,0 +1,604 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = """ +module: nxos_pim_interface +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages PIM interface configuration. +description: +- Manages PIM interface configuration settings. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- When C(state=default), supported params will be reset to a default state. These + include C(dr_prio), C(hello_auth_key), C(hello_interval), C(jp_policy_out), C(jp_policy_in), + C(jp_type_in), C(jp_type_out), C(border), C(neighbor_policy), C(neighbor_type). +- The C(hello_auth_key) param is not idempotent. +- C(hello_auth_key) only supports clear text passwords. +- When C(state=absent), pim interface configuration will be set to defaults and pim-sm + will be disabled on the interface. +- PIM must be enabled on the device to use this module. +- This module is for Layer 3 interfaces. +options: + interface: + description: + - Full name of the interface such as Ethernet1/33. + type: str + required: true + sparse: + description: + - Enable/disable sparse-mode on the interface. + type: bool + default: false + bfd: + description: + - Enables BFD for PIM at the interface level. This overrides the bfd variable + set at the pim global level. + - Valid values are 'enable', 'disable' or 'default'. + - "Dependency: ''feature bfd''" + type: str + choices: + - enable + - disable + - default + dr_prio: + description: + - Configures priority for PIM DR election on interface. + type: str + hello_auth_key: + description: + - Authentication for hellos on this interface. + type: str + hello_interval: + description: + - Hello interval in milliseconds or seconds for this interface. + - Use the option I(hello_interval_ms) to specify if the given value is in + milliseconds or seconds. The default is seconds. + type: int + hello_interval_ms: + description: + - Specifies that the hello_interval is in milliseconds. + - When set to True, this indicates that the user is providing the + hello_interval in milliseconds and hence, no conversion is required. + type: bool + version_added: 2.0.0 + jp_policy_out: + description: + - Policy for join-prune messages (outbound). + type: str + jp_policy_in: + description: + - Policy for join-prune messages (inbound). + type: str + jp_type_out: + description: + - Type of policy mapped to C(jp_policy_out). + type: str + choices: + - prefix + - routemap + jp_type_in: + description: + - Type of policy mapped to C(jp_policy_in). + type: str + choices: + - prefix + - routemap + border: + description: + - Configures interface to be a boundary of a PIM domain. + type: bool + default: false + neighbor_policy: + description: + - Configures a neighbor policy for filtering adjacencies. + type: str + neighbor_type: + description: + - Type of policy mapped to neighbor_policy. + type: str + choices: + - prefix + - routemap + state: + description: + - Manages desired state of the resource. + type: str + choices: + - present + - absent + - default + default: present +""" +EXAMPLES = """ +- name: Ensure PIM is not running on the interface + cisco.nxos.nxos_pim_interface: + interface: eth1/33 + state: absent + +- name: Ensure the interface has pim-sm enabled with the appropriate priority and + hello interval + cisco.nxos.nxos_pim_interface: + interface: eth1/33 + dr_prio: 10 + hello_interval: 40 + state: present + +- name: Ensure join-prune policies exist + cisco.nxos.nxos_pim_interface: + interface: eth1/33 + jp_policy_in: JPIN + jp_policy_out: JPOUT + jp_type_in: routemap + jp_type_out: routemap + +- name: disable bfd on the interface + cisco.nxos.nxos_pim_interface: + interface: eth1/33 + bfd: disable + +- name: Ensure defaults are in place + cisco.nxos.nxos_pim_interface: + interface: eth1/33 + state: default +""" + +RETURN = r""" +commands: + description: command sent to the device + returned: always + type: list + sample: ["interface eth1/33", + "ip pim neighbor-policy test", + "ip pim bfd-instance disable", + "ip pim neighbor-policy test" + ] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + get_interface_type, + load_config, + run_commands, +) + + +PARAM_TO_COMMAND_KEYMAP = { + "interface": "", + "bfd": "ip pim bfd-instance", + "sparse": "ip pim sparse-mode", + "dr_prio": "ip pim dr-priority {0}", + "hello_interval": "ip pim hello-interval {0}", + "hello_auth_key": "ip pim hello-authentication ah-md5 {0}", + "border": "ip pim border", + "jp_policy_out": "ip pim jp-policy prefix-list {0} out", + "jp_policy_in": "ip pim jp-policy prefix-list {0} in", + "jp_type_in": "", + "jp_type_out": "", + "neighbor_policy": "ip pim neighbor-policy prefix-list {0}", + "neighbor_type": "", +} + +PARAM_TO_DEFAULT_KEYMAP = { + "bfd": "default", + "dr_prio": "1", + "hello_interval": "30000", + "sparse": False, + "border": False, + "hello_auth_key": False, +} + +BFD_KEYMAP = { + None: None, + "default": "no ip pim bfd-instance", + "disable": "ip pim bfd-instance disable", + "enable": "ip pim bfd-instance", +} + + +def execute_show_command(command, module, text=False): + if text: + cmds = [{"command": command, "output": "text"}] + else: + cmds = [{"command": command, "output": "json"}] + + return run_commands(module, cmds) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def local_existing(gexisting): + jp_bidir = False + isauth = False + if gexisting: + jp_bidir = gexisting.get("jp_bidir") + isauth = gexisting.get("isauth") + if jp_bidir and isauth: + gexisting.pop("jp_bidir") + gexisting.pop("isauth") + + return gexisting, jp_bidir, isauth + + +def get_interface_mode(interface, intf_type, module): + mode = "unknown" + command = "show interface {0}".format(interface) + body = execute_show_command(command, module) + + try: + interface_table = body[0]["TABLE_interface"]["ROW_interface"] + except (KeyError, AttributeError, IndexError): + return mode + + if intf_type in ["ethernet", "portchannel"]: + mode = str(interface_table.get("eth_mode", "layer3")) + if mode in ["access", "trunk"]: + mode = "layer2" + elif mode == "routed": + mode = "layer3" + elif intf_type in ["loopback", "svi"]: + mode = "layer3" + return mode + + +def get_pim_interface(module, interface): + pim_interface = {} + body = get_config(module, flags=["interface {0}".format(interface)]) + + pim_interface["bfd"] = "default" + pim_interface["neighbor_type"] = None + pim_interface["neighbor_policy"] = None + pim_interface["jp_policy_in"] = None + pim_interface["jp_policy_out"] = None + pim_interface["jp_type_in"] = None + pim_interface["jp_type_out"] = None + pim_interface["jp_bidir"] = False + pim_interface["isauth"] = False + + if body: + all_lines = body.splitlines() + + for each in all_lines: + if "jp-policy" in each: + policy_name = re.search( + r"ip pim jp-policy(?: prefix-list)? (\S+)(?: \S+)?", + each, + ).group(1) + if "prefix-list" in each: + ptype = "prefix" + else: + ptype = "routemap" + if "out" in each: + pim_interface["jp_policy_out"] = policy_name + pim_interface["jp_type_out"] = ptype + elif "in" in each: + pim_interface["jp_policy_in"] = policy_name + pim_interface["jp_type_in"] = ptype + else: + pim_interface["jp_policy_in"] = policy_name + pim_interface["jp_policy_out"] = policy_name + pim_interface["jp_bidir"] = True + elif "neighbor-policy" in each: + pim_interface["neighbor_policy"] = re.search( + r"ip pim neighbor-policy(?: prefix-list)? (\S+)", + each, + ).group(1) + if "prefix-list" in each: + pim_interface["neighbor_type"] = "prefix" + else: + pim_interface["neighbor_type"] = "routemap" + elif "ah-md5" in each: + pim_interface["isauth"] = True + elif "sparse-mode" in each: + pim_interface["sparse"] = True + elif "bfd-instance" in each: + m = re.search(r"ip pim bfd-instance(?P<disable> disable)?", each) + if m: + pim_interface["bfd"] = "disable" if m.group("disable") else "enable" + elif "border" in each: + pim_interface["border"] = True + elif "hello-interval" in each: + pim_interface["hello_interval"] = re.search( + r"ip pim hello-interval (\d+)", + body, + ).group(1) + elif "dr-priority" in each: + pim_interface["dr_prio"] = re.search(r"ip pim dr-priority (\d+)", body).group(1) + + return pim_interface + + +def fix_delta(delta, existing): + for key in list(delta): + if key in ["dr_prio", "hello_interval", "sparse", "border"]: + if delta.get(key) == PARAM_TO_DEFAULT_KEYMAP.get(key) and existing.get(key) is None: + delta.pop(key) + return delta + + +def config_pim_interface(delta, existing, jp_bidir, isauth): + command = None + commands = [] + + delta = fix_delta(delta, existing) + + if jp_bidir: + if delta.get("jp_policy_in") or delta.get("jp_policy_out"): + if existing.get("jp_type_in") == "prefix": + command = "no ip pim jp-policy prefix-list {0}".format(existing.get("jp_policy_in")) + else: + command = "no ip pim jp-policy {0}".format(existing.get("jp_policy_in")) + if command: + commands.append(command) + + for k, v in delta.items(): + if k in [ + "bfd", + "dr_prio", + "hello_interval", + "hello_auth_key", + "border", + "sparse", + ]: + if k == "bfd": + command = BFD_KEYMAP[v] + elif v: + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(v) + elif k == "hello_auth_key": + if isauth: + command = "no ip pim hello-authentication ah-md5" + else: + command = "no " + PARAM_TO_COMMAND_KEYMAP.get(k).format(v) + + if command: + commands.append(command) + elif k in [ + "neighbor_policy", + "jp_policy_in", + "jp_policy_out", + "neighbor_type", + ]: + if k in ["neighbor_policy", "neighbor_type"]: + temp = delta.get("neighbor_policy") or existing.get("neighbor_policy") + if delta.get("neighbor_type") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif delta.get("neighbor_type") == "routemap": + command = "ip pim neighbor-policy {0}".format(temp) + elif existing.get("neighbor_type") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif existing.get("neighbor_type") == "routemap": + command = "ip pim neighbor-policy {0}".format(temp) + elif k in ["jp_policy_in", "jp_type_in"]: + temp = delta.get("jp_policy_in") or existing.get("jp_policy_in") + if delta.get("jp_type_in") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif delta.get("jp_type_in") == "routemap": + command = "ip pim jp-policy {0} in".format(temp) + elif existing.get("jp_type_in") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif existing.get("jp_type_in") == "routemap": + command = "ip pim jp-policy {0} in".format(temp) + elif k in ["jp_policy_out", "jp_type_out"]: + temp = delta.get("jp_policy_out") or existing.get("jp_policy_out") + if delta.get("jp_type_out") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif delta.get("jp_type_out") == "routemap": + command = "ip pim jp-policy {0} out".format(temp) + elif existing.get("jp_type_out") == "prefix": + command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp) + elif existing.get("jp_type_out") == "routemap": + command = "ip pim jp-policy {0} out".format(temp) + if command: + commands.append(command) + command = None + + if "no ip pim sparse-mode" in commands: + # sparse is long-running on some platforms, process it last + commands.remove("no ip pim sparse-mode") + commands.append("no ip pim sparse-mode") + return commands + + +def get_pim_interface_defaults(): + args = dict( + dr_prio=PARAM_TO_DEFAULT_KEYMAP.get("dr_prio"), + bfd=PARAM_TO_DEFAULT_KEYMAP.get("bfd"), + border=PARAM_TO_DEFAULT_KEYMAP.get("border"), + sparse=PARAM_TO_DEFAULT_KEYMAP.get("sparse"), + hello_interval=PARAM_TO_DEFAULT_KEYMAP.get("hello_interval"), + hello_auth_key=PARAM_TO_DEFAULT_KEYMAP.get("hello_auth_key"), + ) + + default = dict((param, value) for (param, value) in args.items() if value is not None) + + return default + + +def default_pim_interface_policies(existing, jp_bidir): + commands = [] + + if jp_bidir: + if existing.get("jp_policy_in") or existing.get("jp_policy_out"): + if existing.get("jp_type_in") == "prefix": + command = "no ip pim jp-policy prefix-list {0}".format(existing.get("jp_policy_in")) + if command: + commands.append(command) + + elif not jp_bidir: + command = None + for k in existing: + if k == "jp_policy_in": + if existing.get("jp_policy_in"): + if existing.get("jp_type_in") == "prefix": + command = "no ip pim jp-policy prefix-list {0} in".format( + existing.get("jp_policy_in"), + ) + else: + command = "no ip pim jp-policy {0} in".format(existing.get("jp_policy_in")) + elif k == "jp_policy_out": + if existing.get("jp_policy_out"): + if existing.get("jp_type_out") == "prefix": + command = "no ip pim jp-policy prefix-list {0} out".format( + existing.get("jp_policy_out"), + ) + else: + command = "no ip pim jp-policy {0} out".format( + existing.get("jp_policy_out"), + ) + if command: + commands.append(command) + command = None + + if existing.get("neighbor_policy"): + command = "no ip pim neighbor-policy" + commands.append(command) + + return commands + + +def config_pim_interface_defaults(existing, jp_bidir, isauth): + command = [] + + # returns a dict + defaults = get_pim_interface_defaults() + delta = dict(set(defaults.items()).difference(existing.items())) + if delta: + # returns a list + command = config_pim_interface(delta, existing, jp_bidir, isauth) + comm = default_pim_interface_policies(existing, jp_bidir) + if comm: + for each in comm: + command.append(each) + + return command + + +def normalize_proposed_values(proposed, module): + keys = proposed.keys() + if "bfd" in keys: + # bfd is a tri-state string: enable, disable, default + proposed["bfd"] = proposed["bfd"].lower() + if "hello_interval" in keys: + hello_interval = proposed["hello_interval"] + if not module.params["hello_interval_ms"]: + hello_interval = hello_interval * 1000 + proposed["hello_interval"] = str(hello_interval) + + +def main(): + argument_spec = dict( + interface=dict(type="str", required=True), + sparse=dict(type="bool", default=False), + dr_prio=dict(type="str"), + hello_auth_key=dict(type="str", no_log=True), + hello_interval=dict(type="int"), + hello_interval_ms=dict(type="bool"), + jp_policy_out=dict(type="str"), + jp_policy_in=dict(type="str"), + jp_type_out=dict(type="str", choices=["prefix", "routemap"]), + jp_type_in=dict(type="str", choices=["prefix", "routemap"]), + bfd=dict(type="str", choices=["enable", "disable", "default"]), + border=dict(type="bool", default=False), + neighbor_policy=dict(type="str"), + neighbor_type=dict(type="str", choices=["prefix", "routemap"]), + state=dict( + type="str", + default="present", + choices=["absent", "default", "present"], + ), + ) + + required_by = {"hello_interval_ms": "hello_interval"} + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_by=required_by, + ) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + state = module.params["state"] + interface = module.params["interface"] + jp_type_in = module.params["jp_type_in"] + jp_type_out = module.params["jp_type_out"] + jp_policy_in = module.params["jp_policy_in"] + jp_policy_out = module.params["jp_policy_out"] + neighbor_policy = module.params["neighbor_policy"] + neighbor_type = module.params["neighbor_type"] + + intf_type = get_interface_type(interface) + if get_interface_mode(interface, intf_type, module) == "layer2": + module.fail_json(msg="this module only works on Layer 3 interfaces.") + + if jp_policy_in: + if not jp_type_in: + module.fail_json(msg="jp_type_in required when using jp_policy_in.") + if jp_policy_out: + if not jp_type_out: + module.fail_json(msg="jp_type_out required when using jp_policy_out.") + if neighbor_policy: + if not neighbor_type: + module.fail_json(msg="neighbor_type required when using neighbor_policy.") + + get_existing = get_pim_interface(module, interface) + existing, jp_bidir, isauth = local_existing(get_existing) + + args = PARAM_TO_COMMAND_KEYMAP.keys() + proposed = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + normalize_proposed_values(proposed, module) + + delta = dict(set(proposed.items()).difference(existing.items())) + + commands = [] + if state == "present": + if delta: + command = config_pim_interface(delta, existing, jp_bidir, isauth) + if command: + commands.append(command) + elif state == "default" or state == "absent": + defaults = config_pim_interface_defaults(existing, jp_bidir, isauth) + if defaults: + commands.append(defaults) + + if commands: + commands.insert(0, ["interface {0}".format(interface)]) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + if "configure" in cmds: + cmds.pop(0) + + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_rp_address.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_rp_address.py new file mode 100644 index 00000000..fe9989ed --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_pim_rp_address.py @@ -0,0 +1,248 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_pim_rp_address +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages configuration of an PIM static RP address instance. +description: +- Manages configuration of an Protocol Independent Multicast (PIM) static rendezvous + point (RP) address instance. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(state=absent) is currently not supported on all platforms. +options: + rp_address: + description: + - Configures a Protocol Independent Multicast (PIM) static rendezvous point (RP) + address. Valid values are unicast addresses. + required: true + type: str + group_list: + description: + - Group range for static RP. Valid values are multicast addresses. + type: str + prefix_list: + description: + - Prefix list policy for static RP. Valid values are prefix-list policy names. + type: str + route_map: + description: + - Route map policy for static RP. Valid values are route-map policy names. + type: str + bidir: + description: + - Group range is treated in PIM bidirectional mode. + type: bool + state: + description: + - Specify desired state of the resource. + default: present + choices: + - present + - absent + type: str +""" +EXAMPLES = """ +- cisco.nxos.nxos_pim_rp_address: + rp_address: 10.1.1.20 + state: present +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf test", "router-id 192.0.2.1"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +def get_existing(module, args, gl): + existing = {} + config = str(get_config(module)) + address = module.params["rp_address"] + + pim_address_re = r"ip pim rp-address (?P<value>.*)$" + for line in re.findall(pim_address_re, config, re.M): + values = line.split() + if values[0] != address: + continue + if gl and "group-list" not in line: + continue + elif not gl and "group-list" in line: + if "224.0.0.0/4" not in line: # ignore default group-list + continue + + existing["bidir"] = existing.get("bidir") or "bidir" in line + if len(values) > 2: + value = values[2] + if values[1] == "route-map": + existing["route_map"] = value + elif values[1] == "prefix-list": + existing["prefix_list"] = value + elif values[1] == "group-list": + if value != "224.0.0.0/4": # ignore default group-list + existing["group_list"] = value + + return existing + + +def state_present(module, existing, proposed, candidate): + address = module.params["rp_address"] + command = "ip pim rp-address {0}".format(address) + if module.params["group_list"] and not proposed.get("group_list"): + command += " group-list " + module.params["group_list"] + if module.params["prefix_list"]: + if not proposed.get("prefix_list"): + command += " prefix-list " + module.params["prefix_list"] + if module.params["route_map"]: + if not proposed.get("route_map"): + command += " route-map " + module.params["route_map"] + commands = build_command(proposed, command) + if commands: + candidate.add(commands, parents=[]) + + +def build_command(param_dict, command): + for param in ["group_list", "prefix_list", "route_map"]: + if param_dict.get(param): + command += " {0} {1}".format(param.replace("_", "-"), param_dict.get(param)) + if param_dict.get("bidir"): + command += " bidir" + return [command] + + +def state_absent(module, existing, candidate): + address = module.params["rp_address"] + + commands = [] + command = "no ip pim rp-address {0}".format(address) + if module.params["group_list"] == existing.get("group_list"): + commands = build_command(existing, command) + elif not module.params["group_list"]: + commands = [command] + + if commands: + candidate.add(commands, parents=[]) + + +def get_proposed(pargs, existing): + proposed = {} + + for key, value in pargs.items(): + if key != "rp_address": + if str(value).lower() == "true": + value = True + elif str(value).lower() == "false": + value = False + + if existing.get(key) != value: + proposed[key] = value + + return proposed + + +def main(): + argument_spec = dict( + rp_address=dict(required=True, type="str"), + group_list=dict(required=False, type="str"), + prefix_list=dict(required=False, type="str"), + route_map=dict(required=False, type="str"), + bidir=dict(required=False, type="bool"), + state=dict(choices=["present", "absent"], default="present", required=False), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ["group_list", "route_map"], + ["group_list", "prefix_list"], + ["route_map", "prefix_list"], + ], + supports_check_mode=True, + ) + + warnings = list() + result = {"changed": False, "commands": [], "warnings": warnings} + + state = module.params["state"] + + args = ["rp_address", "group_list", "prefix_list", "route_map", "bidir"] + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + if module.params["group_list"]: + existing = get_existing(module, args, True) + proposed = get_proposed(proposed_args, existing) + + else: + existing = get_existing(module, args, False) + proposed = get_proposed(proposed_args, existing) + + candidate = CustomNetworkConfig(indent=3) + if state == "present" and (proposed or not existing): + state_present(module, existing, proposed, candidate) + elif state == "absent" and existing: + state_absent(module, existing, candidate) + + if candidate: + candidate = candidate.items_text() + result["commands"] = candidate + result["changed"] = True + msgs = load_config(module, candidate, True) + if msgs: + for item in msgs: + if item: + if isinstance(item, dict): + err_str = item["clierror"] + else: + err_str = item + if "No policy was configured" in err_str: + if state == "absent": + addr = module.params["rp_address"] + new_cmd = "no ip pim rp-address {0}".format(addr) + load_config(module, new_cmd) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_ping.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_ping.py new file mode 100644 index 00000000..31c8517a --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_ping.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_ping +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Tests reachability using ping from Nexus switch. +description: +- Tests reachability using ping from switch to a remote destination. +- For a general purpose network module, see the M(ansible.netcommon.net_ping) module. +- For Windows targets, use the M(ansible.windows.win_ping) module instead. +- For targets running Python, use the M(ansible.builtin.ping) module instead. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +options: + dest: + description: + - IP address or hostname (resolvable by switch) of remote node. + required: true + type: str + count: + description: + - Number of packets to send. + default: 5 + type: int + source: + description: + - Source IP Address or hostname (resolvable by switch) + type: str + vrf: + description: + - Outgoing VRF. + type: str + df_bit: + description: + - Set the DF bit. + default: false + type: bool + size: + description: + - Size of packets to send. + type: int + state: + description: + - Determines if the expected result is success or fail. + choices: + - absent + - present + default: present + type: str +notes: +- Unsupported for Cisco MDS +- For a general purpose network module, see the M(ansible.netcommon.net_ping) module. +- For Windows targets, use the M(ansible.windows.win_ping) module instead. +- For targets running Python, use the M(ansible.builtin.ping) module instead. +""" + +EXAMPLES = """ +- name: Test reachability to 8.8.8.8 using mgmt vrf + cisco.nxos.nxos_ping: + dest: 8.8.8.8 + vrf: management + host: 68.170.147.165 + +- name: Test reachability to a few different public IPs using mgmt vrf + cisco.nxos.nxos_ping: + dest: "{{ item }}" + vrf: management + host: 68.170.147.165 + with_items: + - 8.8.8.8 + - 4.4.4.4 + - 198.6.1.4 + +- name: Test reachability to 8.8.8.8 using mgmt vrf, size and df-bit + cisco.nxos.nxos_ping: + dest: 8.8.8.8 + df_bit: true + size: 1400 + vrf: management +""" + +RETURN = """ +commands: + description: Show the command sent + returned: always + type: list + sample: ["ping 8.8.8.8 count 2 vrf management"] +rtt: + description: Show RTT stats + returned: always + type: dict + sample: {"avg": 6.264, "max": 6.564, "min": 5.978} +packets_rx: + description: Packets successfully received + returned: always + type: int + sample: 2 +packets_tx: + description: Packets successfully transmitted + returned: always + type: int + sample: 2 +packet_loss: + description: Percentage of packets lost + returned: always + type: str + sample: "0.00%" +""" +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import run_commands + + +def get_summary(results_list, reference_point): + summary_string = results_list[reference_point + 1] + summary_list = summary_string.split(",") + + summary = dict( + packets_tx=int(summary_list[0].split("packets")[0].strip()), + packets_rx=int(summary_list[1].split("packets")[0].strip()), + packet_loss=summary_list[2].split("packet")[0].strip(), + ) + + if "bytes from" not in results_list[reference_point - 2]: + ping_pass = False + else: + ping_pass = True + + return summary, ping_pass + + +def get_rtt(results_list, packet_loss, location): + rtt = dict(min=None, avg=None, max=None) + + if packet_loss != "100.00%": + rtt_string = results_list[location] + base = rtt_string.split("=")[1] + rtt_list = base.split("/") + + rtt["min"] = float(rtt_list[0].lstrip()) + rtt["avg"] = float(rtt_list[1]) + rtt["max"] = float(rtt_list[2][:-3]) + + return rtt + + +def get_statistics_summary_line(response_as_list): + for each in response_as_list: + if "---" in each: + index = response_as_list.index(each) + return index + + +def get_ping_results(command, module): + cmd = {"command": command, "output": "text"} + ping = run_commands(module, [cmd])[0] + + if not ping: + module.fail_json( + msg="An unexpected error occurred. Check all params.", + command=command, + destination=module.params["dest"], + vrf=module.params["vrf"], + source=module.params["source"], + size=module.params["size"], + df_bit=module.params["df_bit"], + ) + + elif "can't bind to address" in ping: + module.fail_json(msg="Can't bind to source address.", command=command) + elif "bad context" in ping: + module.fail_json( + msg="Wrong VRF name inserted.", + command=command, + vrf=module.params["vrf"], + ) + else: + splitted_ping = ping.split("\n") + reference_point = get_statistics_summary_line(splitted_ping) + summary, ping_pass = get_summary(splitted_ping, reference_point) + rtt = get_rtt(splitted_ping, summary["packet_loss"], reference_point + 2) + + return (summary, rtt, ping_pass) + + +def main(): + argument_spec = dict( + dest=dict(required=True), + count=dict(required=False, default=5, type="int"), + vrf=dict(required=False), + source=dict(required=False), + size=dict(required=False, type="int"), + df_bit=dict(required=False, default=False, type="bool"), + state=dict(required=False, choices=["present", "absent"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + destination = module.params["dest"] + state = module.params["state"] + size = module.params["size"] + df_bit = module.params["df_bit"] + + ping_command = "ping {0}".format(destination) + for command in ["count", "source", "vrf"]: + arg = module.params[command] + if arg: + ping_command += " {0} {1}".format(command, arg) + + if size: + ping_command += " packet-size {0}".format(size) + + if df_bit: + ping_command += " df-bit" + + summary, rtt, ping_pass = get_ping_results(ping_command, module) + + results = summary + results["rtt"] = rtt + results["commands"] = [ping_command] + + if ping_pass and state == "absent": + module.fail_json(msg="Ping succeeded unexpectedly") + elif not ping_pass and state == "present": + module.fail_json(msg="Ping failed unexpectedly") + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_prefix_lists.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_prefix_lists.py new file mode 100644 index 00000000..cbd2415c --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_prefix_lists.py @@ -0,0 +1,842 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_prefix_lists +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_prefix_lists +short_description: Prefix-Lists resource module. +description: +- This module manages prefix-lists configuration on devices running Cisco NX-OS. +version_added: 2.4.0 +notes: +- Tested against NX-OS 9.3.6. +- Unsupported for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^ip(.*) prefix-list'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of prefix-list configuration. + type: list + elements: dict + suboptions: + afi: + description: + - The Address Family Identifier (AFI) for the prefix-lists. + type: str + choices: ["ipv4", "ipv6"] + prefix_lists: + description: List of prefix-list configurations. + type: list + elements: dict + suboptions: + name: + description: Name of the prefix-list. + type: str + description: + description: Description of the prefix list + type: str + entries: + description: List of configurations for the specified prefix-list + type: list + elements: dict + suboptions: + sequence: + description: Sequence Number. + type: int + action: + description: Prefix-List permit or deny. + type: str + choices: ["permit", "deny"] + prefix: + description: IP or IPv6 prefix in A.B.C.D/LEN or A:B::C:D/LEN format. + type: str + eq: + description: Exact prefix length to be matched. + type: int + ge: + description: Minimum prefix length to be matched. + type: int + le: + description: Maximum prefix length to be matched. + type: int + mask: + description: Explicit match mask. + type: str + state: + description: + - The state the configuration should be left in. + - Refer to examples for more details. + - With state I(replaced), for the listed prefix-lists, + sequences that are in running-config but not in the task are negated. + - With state I(overridden), all prefix-lists that are in running-config but + not in the task are negated. + - Please refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AllowPrefix + description: allows engineering IPv4 networks + entries: + - sequence: 10 + action: permit + prefix: 192.0.2.0/23 + eq: 24 + - sequence: 20 + action: permit + prefix: 198.51.100.128/26 + - name: DenyPrefix + description: denies lab IPv4 networks + entries: + - sequence: 20 + action: deny + prefix: 203.0.113.0/24 + le: 25 + + - afi: ipv6 + prefix_lists: + - name: AllowIPv6Prefix + description: allows engineering IPv6 networks + entries: + - sequence: 8 + action: permit + prefix: "2001:db8:400::/38" + - sequence: 20 + action: permit + prefix: "2001:db8:8000::/35" + le: 37 + +# Task output +# ------------- +# before: [] +# +# commands: +# - "ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks" +# - "ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38" +# - "ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37" +# - "ip prefix-list AllowPrefix description allows engineering IPv4 networks" +# - "ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24" +# - "ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26" +# - "ip prefix-list DenyPrefix description denies lab IPv4 networks" +# - "ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25" +# +# after: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Replace prefix-lists configurations of listed prefix-lists with provided configurations + cisco.nxos.nxos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AllowPrefix + description: allows engineering IPv4 networks + entries: + - sequence: 10 + action: permit + prefix: 203.0.113.64/27 + + - sequence: 30 + action: permit + prefix: 203.0.113.96/27 + - name: AllowPrefix2Stub + description: allow other engineering IPv4 network + state: replaced + +# Task output +# ------------- +# before: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# commands: +# - "no ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24" +# - "ip prefix-list AllowPrefix seq 10 permit 203.0.113.64/27" +# - "ip prefix-list AllowPrefix seq 30 permit 203.0.113.96/27" +# - "no ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26" +# - "ip prefix-list AllowPrefix2Stub description allow other engineering IPv4 network" +# +# after: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 203.0.113.64/27 +# - sequence: 30 +# action: permit +# prefix: 203.0.113.96/27 +# name: AllowPrefix +# - description: allow other engineering IPv4 network +# name: AllowPrefix2Stub +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 203.0.113.64/27 +# ip prefix-list AllowPrefix seq 30 permit 203.0.113.96/27 +# ip prefix-list AllowPrefix2Stub description allow other engineering IPv4 network +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +# Using overridden + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Override all prefix-lists configuration with provided configuration + cisco.nxos.nxos_prefix_lists: &id003 + config: + - afi: ipv4 + prefix_lists: + - name: AllowPrefix + description: allows engineering IPv4 networks + entries: + - sequence: 10 + action: permit + prefix: 203.0.113.64/27 + + - sequence: 30 + action: permit + prefix: 203.0.113.96/27 + - name: AllowPrefix2Stub + description: allow other engineering IPv4 network + state: overridden + +# Task output +# ------------- +# before: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# commands: +# - "no ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24" +# - "ip prefix-list AllowPrefix seq 10 permit 203.0.113.64/27" +# - "ip prefix-list AllowPrefix seq 30 permit 203.0.113.96/27" +# - "no ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26" +# - "ip prefix-list AllowPrefix2Stub description allow other engineering IPv4 network" +# - "no ip prefix-list DenyPrefix" +# - "no ipv6 prefix-list AllowIPv6Prefix" +# +# after: +# - afi: ipv4 +# prefix_lists: +# - name: AllowPrefix +# description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 203.0.113.64/27 +# +# - sequence: 30 +# action: permit +# prefix: 203.0.113.96/27 +# - name: AllowPrefix2Stub +# description: allow other engineering IPv4 network +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 203.0.113.64/27 +# ip prefix-list AllowPrefix seq 30 permit 203.0.113.96/27 +# ip prefix-list AllowPrefix2Stub description allow other engineering IPv4 network + +# Using deleted to delete a all prefix lists for an AFI + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Delete all prefix-lists for an AFI + cisco.nxos.nxos_prefix_lists: + config: + - afi: ipv4 + state: deleted + register: result + +# Task output +# ------------- +# before: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# commands: +# - "no ip prefix-list AllowPrefix" +# - "no ip prefix-list DenyPrefix" +# +# after: +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +# Using deleted to delete a single prefix-list + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Delete a single prefix-list + cisco.nxos.nxos_prefix_lists: + config: + - afi: ipv4 + prefix_lists: + - name: AllowPrefix + state: deleted + +# Task output +# ------------- +# before: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# commands: +# - "no ip prefix-list AllowPrefix" +# +# after: +# - afi: ipv4 +# prefix_lists: +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +# Using deleted to delete all prefix-lists from the device + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Delete all prefix-lists + cisco.nxos.nxos_prefix_lists: + state: deleted + +# Task output +# ------------- +# before: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +# +# commands: +# - "no ip prefix-list AllowPrefix" +# - "no ip prefix-list DenyPrefix" +# - "no ipv6 prefix-list AllowIPv6Prefix" +# +# after: [] +# +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section 'ip(.*) prefix-list' +# nxos-9k-rdo# + +# Using rendered + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_prefix_lists: &id001 + config: + - afi: ipv4 + prefix_lists: + - name: AllowPrefix + description: allows engineering IPv4 networks + entries: + - sequence: 10 + action: permit + prefix: 192.0.2.0/23 + eq: 24 + - sequence: 20 + action: permit + prefix: 198.51.100.128/26 + - name: DenyPrefix + description: denies lab IPv4 networks + entries: + - sequence: 20 + action: deny + prefix: 203.0.113.0/24 + le: 25 + + - afi: ipv6 + prefix_lists: + - name: AllowIPv6Prefix + description: allows engineering IPv6 networks + entries: + - sequence: 8 + action: permit + prefix: "2001:db8:400::/38" + - sequence: 20 + action: permit + prefix: "2001:db8:8000::/35" + le: 37 + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix + +# Using parsed + +# parsed.cfg +# ------------ +# ip prefix-list AllowPrefix description allows engineering IPv4 networks +# ip prefix-list AllowPrefix seq 10 permit 192.0.2.0/23 eq 24 +# ip prefix-list AllowPrefix seq 20 permit 198.51.100.128/26 +# ip prefix-list DenyPrefix description denies lab IPv4 networks +# ip prefix-list DenyPrefix seq 20 deny 203.0.113.0/24 le 25 +# ipv6 prefix-list AllowIPv6Prefix description allows engineering IPv6 networks +# ipv6 prefix-list AllowIPv6Prefix seq 8 permit 2001:db8:400::/38 +# ipv6 prefix-list AllowIPv6Prefix seq 20 permit 2001:db8:8000::/35 le 37 + +- name: Parse externally provided prefix-lists configuration + register: result + cisco.nxos.nxos_prefix_lists: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# - afi: ipv4 +# prefix_lists: +# - description: allows engineering IPv4 networks +# entries: +# - sequence: 10 +# action: permit +# prefix: 192.0.2.0/23 +# eq: 24 +# - sequence: 20 +# action: permit +# prefix: 198.51.100.128/26 +# name: AllowPrefix +# - description: denies lab IPv4 networks +# entries: +# - sequence: 20 +# action: deny +# prefix: 203.0.113.0/24 +# le: 25 +# name: DenyPrefix +# +# - afi: ipv6 +# prefix_lists: +# - description: allows engineering IPv6 networks +# entries: +# - sequence: 8 +# action: permit +# prefix: "2001:db8:400::/38" +# - sequence: 20 +# action: permit +# prefix: "2001:db8:8000::/35" +# le: 37 +# name: AllowIPv6Prefix +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.prefix_lists.prefix_lists import ( + Prefix_listsArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.prefix_lists.prefix_lists import ( + Prefix_lists, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Prefix_listsArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Prefix_lists(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_reboot.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_reboot.py new file mode 100644 index 00000000..51d0d70a --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_reboot.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_reboot +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Reboot a network device. +description: +- Reboot a network device. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Tested against Cisco MDS NX-OS 9.2(1) +- The module will fail due to timeout issues, but the reboot will be performed anyway. +options: + confirm: + description: + - Safeguard boolean. Set to true if you're sure you want to reboot. + required: false + default: false + type: bool +""" + +EXAMPLES = """ +- cisco.nxos.nxos_reboot: + confirm: true +""" + +RETURN = """ +rebooted: + description: Whether the device was instructed to reboot. + returned: success + type: bool + sample: true +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import load_config + + +def reboot(module): + cmds = "terminal dont-ask ; reload" + opts = {"ignore_timeout": True} + load_config(module, cmds, False, opts) + + +def main(): + argument_spec = dict(confirm=dict(default=False, type="bool")) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = dict(changed=False, warnings=warnings) + + if module.params["confirm"]: + if not module.check_mode: + reboot(module) + results["changed"] = True + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_rollback.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_rollback.py new file mode 100644 index 00000000..4b26919d --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_rollback.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_rollback +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Set a checkpoint or rollback to a checkpoint. +description: +- This module offers the ability to set a configuration checkpoint file or rollback + to a configuration checkpoint file on Cisco NXOS switches. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Sometimes C(transport=nxapi) may cause a timeout error. +options: + checkpoint_file: + description: + - Name of checkpoint file to create. Mutually exclusive with rollback_to. + type: str + rollback_to: + description: + - Name of checkpoint file to rollback to. Mutually exclusive with checkpoint_file. + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_rollback: + checkpoint_file: backup.cfg + username: '{{ un }}' + password: '{{ pwd }}' + host: '{{ inventory_hostname }}' +- cisco.nxos.nxos_rollback: + rollback_to: backup.cfg + username: '{{ un }}' + password: '{{ pwd }}' + host: '{{ inventory_hostname }}' +""" + +RETURN = """ +filename: + description: The filename of the checkpoint/rollback file. + returned: success + type: str + sample: 'backup.cfg' +status: + description: Which operation took place and whether it was successful. + returned: success + type: str + sample: 'rollback executed' +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import run_commands + + +def checkpoint(filename, module): + commands = [ + {"command": "terminal dont-ask", "output": "text"}, + {"command": "checkpoint file %s" % filename, "output": "text"}, + ] + run_commands(module, commands) + + +def rollback(filename, module): + commands = [ + { + "command": "rollback running-config file %s" % filename, + "output": "text", + }, + ] + run_commands(module, commands) + + +def main(): + argument_spec = dict(checkpoint_file=dict(required=False), rollback_to=dict(required=False)) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[["checkpoint_file", "rollback_to"]], + supports_check_mode=False, + ) + + checkpoint_file = module.params["checkpoint_file"] + rollback_to = module.params["rollback_to"] + + status = None + filename = None + changed = False + + if checkpoint_file: + checkpoint(checkpoint_file, module) + status = "checkpoint file created" + elif rollback_to: + rollback(rollback_to, module) + status = "rollback executed" + changed = True + filename = rollback_to or checkpoint_file + + module.exit_json(changed=changed, status=status, filename=filename) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_route_maps.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_route_maps.py new file mode 100644 index 00000000..111cc4da --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_route_maps.py @@ -0,0 +1,1648 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_route_maps +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_route_maps +short_description: Route Maps resource module. +description: +- This module manages route maps configuration on devices running Cisco NX-OS. +version_added: 2.2.0 +notes: +- Tested against NX-OS 9.3.6. +- Unsupported for Cisco MDS +- This module works with connection C(network_cli) and C(httpapi). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^route-map'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A list of route-map configuration. + type: list + elements: dict + suboptions: + route_map: + description: Route-map name. + type: str + entries: + description: List of entries (identified by sequence number) for this route-map. + type: list + elements: dict + suboptions: + sequence: + description: Sequence to insert to/delete from existing route-map entry. + type: int + action: + description: Route map denies or permits set operations. + type: str + choices: ["deny", "permit"] + continue_sequence: + description: Continue on a different entry within the route-map. + type: int + description: + description: Description of the route-map. + type: str + match: + description: Match values from routing table. + type: dict + suboptions: + as_number: + description: Match BGP peer AS number. + type: dict + suboptions: + asn: + description: AS number. + type: list + elements: str + as_path_list: + description: AS path access list name. + type: list + elements: str + as_path: + description: Match BGP AS path access-list. + type: list + elements: str + community: + description: Match BGP community list. + type: dict + suboptions: + community_list: + description: Community list. + type: list + elements: str + exact_match: + description: Do exact matching of communities. + type: bool + evpn: + description: Match BGP EVPN Routes. + type: dict + suboptions: + route_types: + description: Match route type for evpn route. + type: list + elements: str + extcommunity: + description: Match BGP community list. + type: dict + suboptions: + extcommunity_list: + description: Extended Community list. + type: list + elements: str + exact_match: + description: Do exact matching of extended communities. + type: bool + interfaces: + description: Match first hop interface of route. + type: list + elements: str + ip: + description: Configure IP specific information. + type: dict + suboptions: &id001 + address: + description: Match address of route or match packet. + type: dict + suboptions: + access_list: + description: IP access-list name (for use in route-maps for PBR only). + type: str + prefix_lists: + description: Match entries of prefix-lists. + type: list + elements: str + multicast: + description: Match multicast attributes. + type: dict + suboptions: + source: + description: Multicast source address. + type: str + group: + description: + - Multicast Group prefix. + - Mutually exclusive with group_range. + type: dict + suboptions: + prefix: + description: IPv4 group prefix. + type: str + group_range: + description: + - Multicast Group address range. + - Mutually exclusive with group. + type: dict + suboptions: + first: + description: First Group address. + type: str + last: + description: Last Group address. + type: str + rp: + description: Rendezvous point. + type: dict + suboptions: + prefix: + description: IPv4 rendezvous prefix. + type: str + rp_type: + description: Multicast rendezvous point type. + type: str + choices: ["ASM", "Bidir"] + next_hop: + description: Match next-hop address of route. + type: dict + suboptions: + prefix_lists: + description: Match entries of prefix-lists. + type: list + elements: str + route_source: + description: Match advertising source address of route. + type: dict + suboptions: + prefix_lists: + description: Match entries of prefix-lists. + type: list + elements: str + ipv6: + description: Configure IPv6 specific information. + type: dict + suboptions: *id001 + mac_list: + description: Match entries of mac-lists. + type: list + elements: str + metric: + description: Match metric of route. + type: list + elements: int + ospf_area: + description: Match ospf area. + type: list + elements: int + route_types: + description: Match route-type of route. + type: list + elements: str + choices: ["external", "inter-area", "internal", "intra-area", "level-1", "level-2", "local", "nssa-external", "type-1", "type-2"] + source_protocol: + description: Match source protocol. + type: list + elements: str + tags: + description: Match tag of route. + type: list + elements: int + set: + description: Set values in destination routing protocol. + type: dict + suboptions: + as_path: + description: Prepend string for a BGP AS-path attribute. + type: dict + suboptions: + prepend: + description: Prepend to the AS-Path. + type: dict + suboptions: + as_number: + description: AS number. + type: list + elements: str + last_as: + description: Number of last-AS prepends. + type: int + tag: + description: Set the tag as an AS-path attribute. + type: bool + comm_list: + description: Set BGP community list (for deletion). + type: str + community: + description: Set BGP community attribute. + type: dict + suboptions: + additive: + description: Add to existing community. + type: bool + graceful_shutdown: + description: Graceful Shutdown (well-known community). + type: bool + internet: + description: Internet (well-known community). + type: bool + local_as: + description: Do not send outside local AS (well-known community). + type: bool + no_advertise: + description: Do not advertise to any peer (well-known community). + type: bool + no_export: + description: Do not export to next AS (well-known community). + type: bool + number: + description: "Community number aa:nn format" + type: list + elements: str + dampening: + description: Set BGP route flap dampening parameters. + type: dict + suboptions: + half_life: + description: Half-life time for the penalty. + type: int + start_reuse_route: + description: Value to start reusing a route. + type: int + start_suppress_route: + description: Value to start suppressing a route. + type: int + max_suppress_time: + description: Maximum suppress time for stable route. + type: int + distance: + description: Configure administrative distance. + type: dict + suboptions: + igp_ebgp_routes: + description: Administrative distance for IGP or EBGP routes + type: int + internal_routes: + description: Distance for internal routes. + type: int + local_routes: + description: Distance for local routes. + type: int + evpn: + description: Set BGP EVPN Routes. + type: dict + suboptions: + gateway_ip: + description: + - Set gateway IP for type 5 EVPN routes. + - Cannot set ip and use-nexthop in the same route-map sequence. + type: dict + suboptions: + ip: + description: Gateway IP address. + type: str + use_nexthop: + description: Use nexthop address as gateway IP. + type: bool + extcomm_list: + description: Set BGP extcommunity list (for deletion). + type: str + forwarding_address: + description: Set the forwarding address. + type: bool + null_interface: + description: Output Null interface. + type: str + ip: + description: Configure IP features. + type: dict + suboptions: &id002 + address: + description: Specify IP address. + type: dict + suboptions: + prefix_list: + description: Name of prefix list (Max Size 63). + type: str + precedence: + description: Set precedence field. + type: str + ipv6: + description: Configure IPv6 features. + type: dict + suboptions: *id002 + label_index: + description: Set Segment Routing (SR) label index of route. + type: int + level: + description: Where to import route. + type: str + choices: ["level-1", "level-1-2", "level-2"] + local_preference: + description: BGP local preference path attribute. + type: int + metric: + description: Set metric for destination routing protocol. + type: dict + suboptions: + bandwidth: + description: Metric value or Bandwidth in Kbits per second (Max Size 11). + type: int + igrp_delay_metric: + description: IGRP delay metric. + type: int + igrp_reliability_metric: + description: IGRP reliability metric where 255 is 100 percent reliable. + type: int + igrp_effective_bandwidth_metric: + description: IGRP Effective bandwidth metric (Loading) 255 is 100%. + type: int + igrp_mtu: + description: IGRP MTU of the path. + type: int + metric_type: + description: Type of metric for destination routing protocol. + type: str + choices: ["external", "internal", "type-1", "type-2"] + nssa_only: + description: OSPF NSSA Areas. + type: bool + origin: + description: BGP origin code. + type: str + choices: ["egp", "igp", "incomplete"] + path_selection: + description: Path selection criteria for BGP. + type: str + choices: ["all", "backup", "best2", "multipaths"] + tag: + description: Tag value for destination routing protocol. + type: int + weight: + description: BGP weight for routing table. + type: int + state: + description: + - The state the configuration should be left in. + - With state I(replaced), for the listed route-maps, + sequences that are in running-config but not in the task are negated. + - With state I(overridden), all route-maps that are in running-config but + not in the task are negated. + - Please refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config | section "^route-map" +# nxos-9k-rdo# + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_route_maps: + config: + - route_map: rmap1 + entries: + - sequence: 10 + action: permit + description: rmap1-10-permit + match: + ip: + address: + access_list: acl_1 + as_path: Allow40 + as_number: + asn: 65564 + + - sequence: 20 + action: deny + description: rmap1-20-deny + match: + community: + community_list: + - BGPCommunity1 + - BGPCommunity2 + ip: + address: + prefix_lists: + - AllowPrefix1 + - AllowPrefix2 + set: + dampening: + half_life: 30 + start_reuse_route: 1500 + start_suppress_route: 10000 + max_suppress_time: 120 + + - route_map: rmap2 + entries: + - sequence: 20 + action: permit + description: rmap2-20-permit + continue_sequence: 40 + match: + ipv6: + address: + prefix_lists: AllowIPv6Prefix + interfaces: "{{ nxos_int1 }}" + set: + as_path: + prepend: + as_number: + - 65563 + - 65568 + - 65569 + comm_list: BGPCommunity + + - sequence: 40 + action: deny + description: rmap2-40-deny + match: + route_types: + - level-1 + - level-2 + tags: 2 + ip: + multicast: + rp: + prefix: 192.0.2.0/24 + rp_type: ASM + source: 203.0.113.0/24 + group_range: + first: 239.0.0.1 + last: 239.255.255.255 + state: merged + +# Task output +# ------------- +# before: [] +# +# commands: +# - "route-map rmap1 permit 10" +# - "match as-number 65564" +# - "match as-path Allow40" +# - "match ip address acl_1" +# - "description rmap1-10-permit" +# - "route-map rmap1 deny 20" +# - "match community BGPCommunity1 BGPCommunity2" +# - "match ip address prefix-list AllowPrefix1 AllowPrefix2" +# - "description rmap1-20-deny" +# - "set dampening 30 1500 10000 120" +# - "route-map rmap2 permit 20" +# - "match interface Ethernet1/1" +# - "match ipv6 address prefix-list AllowIPv6Prefix" +# - "set as-path prepend 65563 65568 65569" +# - "description rmap2-20-permit" +# - "continue 40" +# - "set comm-list BGPCommunity delete" +# - "route-map rmap2 deny 40" +# - "match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM" +# - "match route-type level-1 level-2" +# - "match tag 2" +# - "description rmap2-40-deny" +# +# after: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +# Using replaced +# (for the listed route-map(s), sequences that are in running-config but not in the task are negated) + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +- name: Replace route-maps configurations of listed route-maps with provided configurations + cisco.nxos.nxos_route_maps: + config: + - route_map: rmap1 + entries: + - sequence: 20 + action: deny + description: rmap1-20-deny + match: + community: + community_list: + - BGPCommunity4 + - BGPCommunity5 + ip: + address: + prefix_lists: + - AllowPrefix1 + set: + community: + local_as: True + state: replaced + +# Task output +# ------------- +# before: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# +# commands: +# - no route-map rmap1 permit 10 +# - route-map rmap1 deny 20 +# - no match community BGPCommunity1 BGPCommunity2 +# - match community BGPCommunity4 BGPCommunity5 +# - no match ip address prefix-list AllowPrefix1 AllowPrefix2 +# - match ip address prefix-list AllowPrefix1 +# - no set dampening 30 1500 10000 120 +# - set community local-AS +# +# after: +# - route_map: rmap1 +# entries: +# - sequence: 20 +# action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity4 +# - BGPCommunity5 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# set: +# community: +# local_as: True +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 deny 20 +# description rmap1-20-deny +# match community BGPCommunity4 BGPCommunity5 +# match ip address prefix-list AllowPrefix1 +# set community local-AS +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +# Using overridden + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +- name: Override all route-maps configuration with provided configuration + cisco.nxos.nxos_route_maps: + config: + - route_map: rmap1 + entries: + - sequence: 20 + action: deny + description: rmap1-20-deny + match: + community: + community_list: + - BGPCommunity4 + - BGPCommunity5 + ip: + address: + prefix_lists: + - AllowPrefix1 + set: + community: + local_as: True + state: overridden + +# Task output +# ------------- +# before: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# +# commands: +# - no route-map rmap1 permit 10 +# - route-map rmap1 deny 20 +# - no match community BGPCommunity1 BGPCommunity2 +# - match community BGPCommunity4 BGPCommunity5 +# - no match ip address prefix-list AllowPrefix1 AllowPrefix2 +# - match ip address prefix-list AllowPrefix1 +# - no set dampening 30 1500 10000 120 +# - set community local-AS +# - no route-map rmap2 permit 20 +# - no route-map rmap2 deny 40 +# +# after: +# - route_map: rmap1 +# entries: +# - sequence: 20 +# action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity4 +# - BGPCommunity5 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# set: +# community: +# local_as: True +# +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^route-map" +# route-map rmap1 deny 20 +# description rmap1-20-deny +# match community BGPCommunity4 BGPCommunity5 +# match ip address prefix-list AllowPrefix1 +# set community local-AS + +# Using deleted to delete a single route-map + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +- name: Delete single route-map + cisco.nxos.nxos_route_maps: + config: + - route_map: rmap1 + state: deleted + +# Task output +# ------------- +# before: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# +# commands: +# - no route-map rmap1 permit 10 +# - no route-map rmap1 deny 20 +# +# after: +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^route-map" +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +# Using deleted to delete all route-maps from the device running-config + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +- name: Delete all route-maps + cisco.nxos.nxos_route_maps: + state: deleted + +# Task output +# ------------- +# before: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 +# +# commands: +# - no route-map rmap1 permit 10 +# - no route-map rmap1 deny 20 +# - no route-map rmap2 permit 20 +# - no route-map rmap2 deny 40 +# +# after: [] +# +# After state: +# ------------ +# nxos-9k-rdo# sh running-config | section "^route-map" + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_route_maps: + config: + - route_map: rmap1 + entries: + - sequence: 10 + action: permit + description: rmap1-10-permit + match: + ip: + address: + access_list: acl_1 + as_path: Allow40 + as_number: + asn: 65564 + + - sequence: 20 + action: deny + description: rmap1-20-deny + match: + community: + community_list: + - BGPCommunity1 + - BGPCommunity2 + ip: + address: + prefix_lists: + - AllowPrefix1 + - AllowPrefix2 + set: + dampening: + half_life: 30 + start_reuse_route: 1500 + start_suppress_route: 10000 + max_suppress_time: 120 + + - route_map: rmap2 + entries: + - sequence: 20 + action: permit + description: rmap2-20-permit + continue_sequence: 40 + match: + ipv6: + address: + prefix_lists: AllowIPv6Prefix + interfaces: "{{ nxos_int1 }}" + set: + as_path: + prepend: + as_number: + - 65563 + - 65568 + - 65569 + comm_list: BGPCommunity + + - sequence: 40 + action: deny + description: rmap2-40-deny + match: + route_types: + - level-1 + - level-2 + tags: 2 + ip: + multicast: + rp: + prefix: 192.0.2.0/24 + rp_type: ASM + source: 203.0.113.0/24 + group_range: + first: 239.0.0.1 + last: 239.255.255.255 + state: rendered + +# Task Output (redacted) +# ----------------------- +# rendered: +# - "route-map rmap1 permit 10" +# - "match as-number 65564" +# - "match as-path Allow40" +# - "match ip address acl_1" +# - "description rmap1-10-permit" +# - "route-map rmap1 deny 20" +# - "match community BGPCommunity1 BGPCommunity2" +# - "match ip address prefix-list AllowPrefix1 AllowPrefix2" +# - "description rmap1-20-deny" +# - "set dampening 30 1500 10000 120" +# - "route-map rmap2 permit 20" +# - "match interface Ethernet1/1" +# - "match ipv6 address prefix-list AllowIPv6Prefix" +# - "set as-path prepend 65563 65568 65569" +# - "description rmap2-20-permit" +# - "continue 40" +# - "set comm-list BGPCommunity delete" +# - "route-map rmap2 deny 40" +# - "match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM" +# - "match route-type level-1 level-2" +# - "match tag 2" +# - "description rmap2-40-deny" + +# Using parsed + +# parsed.cfg +# ------------ +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap1 deny 20 +# match community BGPCommunity1 BGPCommunity2 +# match ip address prefix-list AllowPrefix1 AllowPrefix2 +# description rmap1-20-deny +# set dampening 30 1500 10000 120 +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete +# route-map rmap2 deny 40 +# match ip multicast source 203.0.113.0/24 group-range 239.0.0.1 to 239.255.255.255 rp 192.0.2.0/24 rp-type ASM +# match route-type level-1 level-2 +# match tag 2 +# description rmap2-40-deny + +- name: Parse externally provided route-maps configuration + cisco.nxos.nxos_route_maps: + running_config: "{{ lookup('file', './fixtures/parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - action: deny +# description: rmap1-20-deny +# match: +# community: +# community_list: +# - BGPCommunity1 +# - BGPCommunity2 +# ip: +# address: +# prefix_lists: +# - AllowPrefix1 +# - AllowPrefix2 +# sequence: 20 +# set: +# dampening: +# half_life: 30 +# max_suppress_time: 120 +# start_reuse_route: 1500 +# start_suppress_route: 10000 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +# - action: deny +# description: rmap2-40-deny +# match: +# ip: +# multicast: +# group_range: +# first: 239.0.0.1 +# last: 239.255.255.255 +# rp: +# prefix: 192.0.2.0/24 +# rp_type: ASM +# source: 203.0.113.0/24 +# route_types: +# - level-1 +# - level-2 +# tags: +# - 2 +# sequence: 40 + +# Using gathered + +# Existing route-map config +# --------------------------- +# nxos-9k-rdo# show running-config | section "^route-map" +# route-map rmap1 permit 10 +# match as-number 65564 +# match as-path Allow40 +# match ip address acl_1 +# description rmap1-10-permit +# route-map rmap2 permit 20 +# match interface Ethernet1/1 +# match ipv6 address prefix-list AllowIPv6Prefix +# set as-path prepend 65563 65568 65569 +# description rmap2-20-permit +# continue 40 +# set comm-list BGPCommunity delete + +- name: Gather route-maps facts using gathered + cisco.nxos.nxos_route_maps: + state: gathered + +# gathered: +# - route_map: rmap1 +# entries: +# - action: permit +# description: rmap1-10-permit +# match: +# as_number: +# asn: +# - '65564' +# as_path: +# - Allow40 +# ip: +# address: +# access_list: acl_1 +# sequence: 10 +# +# - route_map: rmap2 +# entries: +# - action: permit +# continue_sequence: 40 +# description: rmap2-20-permit +# match: +# interfaces: +# - Ethernet1/1 +# ipv6: +# address: +# prefix_lists: +# - AllowIPv6Prefix +# sequence: 20 +# set: +# as_path: +# prepend: +# as_number: +# - '65563' +# - '65568' +# - '65569' +# comm_list: BGPCommunity +# +""" + +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: + - "route-map rmap1 permit 10" + - "match as-number 65564" + - "match as-path Allow40" + - "match ip address acl_1" + - "description rmap1-10-permit" + - "route-map rmap1 deny 20" + - "match community BGPCommunity1 BGPCommunity2" +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.route_maps.route_maps import ( + Route_mapsArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.route_maps.route_maps import ( + Route_maps, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Route_mapsArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Route_maps(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_rpm.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_rpm.py new file mode 100644 index 00000000..7d7ab4db --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_rpm.py @@ -0,0 +1,396 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_rpm +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Install patch or feature rpms on Cisco NX-OS devices. +description: +- Install software maintenance upgrade (smu) RPMS and 3rd party RPMS on Cisco NX-OS + devices. +version_added: 1.0.0 +author: Sai Chintalapudi (@saichint) +notes: +- Tested against NXOSv 7.0(3)I2(5), 7.0(3)I4(6), 7.0(3)I5(3), 7.0(3)I6(1), 7.0(3)I7(3) +- Unsupported for Cisco MDS +- For patches, the minimum platform version needed is 7.0(3)I2(5) +- For feature rpms, the minimum platform version needed is 7.0(3)I6(1) +- The module manages the entire RPM lifecycle (Add, activate, commit, deactivate, + remove) +- For reload patches, this module is NOT idempotent until the patch is committed. +options: + pkg: + description: + - Name of the RPM package. + type: str + file_system: + description: + - The remote file system of the device. If omitted, devices that support a file_system + parameter will use their default values. + default: bootflash + type: str + aggregate: + description: + - List of RPM/patch definitions. + type: list + elements: dict + suboptions: + pkg: + description: + - Name of the RPM package. + required: True + type: str + file_system: + description: + - The remote file system of the device. If omitted, devices that support a file_system + parameter will use their default values. + type: str + state: + description: + - If the state is present, the rpm will be installed, If the state is absent, + it will be removed. + choices: + - present + - absent + type: str + state: + description: + - If the state is present, the rpm will be installed, If the state is absent, + it will be removed. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_rpm: + pkg: nxos.sample-n9k_ALL-1.0.0-7.0.3.I7.3.lib32_n9000.rpm +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["install add bootflash:nxos.sample-n9k_ALL-1.0.0-7.0.3.I7.3.lib32_n9000.rpm forced", + "install activate nxos.sample-n9k_ALL-1.0.0-7.0.3.I7.3.lib32_n9000 forced", + "install commit nxos.sample-n9k_ALL-1.0.0-7.0.3.I7.3.lib32_n9000"] +""" + + +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + iteration = 0 + cmds = [{"command": command, "output": "text"}] + + while iteration < 10: + body = run_commands(module, cmds)[0] + if body: + return body + else: + time.sleep(2) + iteration += 1 + + +def remote_file_exists(module, dst, file_system): + command = "dir {0}:/{1}".format(file_system, dst) + body = execute_show_command(command, module) + if "No such file" in body: + return False + return True + + +def config_cmd_operation(module, cmd): + iteration = 0 + while iteration < 10: + msg = load_config(module, [cmd], True) + if msg: + if ( + "another install operation is in progress" in msg[0].lower() + or "failed" in msg[0].lower() + ): + time.sleep(2) + iteration += 1 + else: + return + else: + return + + +def validate_operation(module, show_cmd, cfg_cmd, pkg, pkg_not_present): + iteration = 0 + while iteration < 10: + body = execute_show_command(show_cmd, module) + if pkg_not_present: + if pkg not in body: + return + else: + if pkg in body: + return + time.sleep(2) + iteration += 1 + + err = 'Operation "{0}" Failed'.format(cfg_cmd) + module.fail_json(msg=err) + + +def add_operation(module, show_cmd, file_system, full_pkg, pkg): + cmd = "install add {0}:{1}".format(file_system, full_pkg) + config_cmd_operation(module, cmd) + validate_operation(module, show_cmd, cmd, pkg, False) + return cmd + + +def activate_operation(module, show_cmd, pkg): + cmd = "install activate {0} forced".format(pkg) + config_cmd_operation(module, cmd) + validate_operation(module, show_cmd, cmd, pkg, False) + return cmd + + +def activate_reload(module, pkg, flag): + iteration = 0 + if flag: + cmd = "install activate {0} forced".format(pkg) + else: + cmd = "install deactivate {0} forced".format(pkg) + opts = {"ignore_timeout": True} + while iteration < 10: + msg = load_config(module, [cmd], True, opts) + if msg: + if isinstance(msg[0], int): + if msg[0] == -32603: + return cmd + elif isinstance(msg[0], str): + if "socket is closed" in msg[0].lower(): + return cmd + if ( + "another install operation is in progress" in msg[0].lower() + or "failed" in msg[0].lower() + ): + time.sleep(2) + iteration += 1 + + +def commit_operation(module, show_cmd, pkg, flag): + cmd = "install commit {0}".format(pkg) + config_cmd_operation(module, cmd) + validate_operation(module, show_cmd, cmd, pkg, flag) + return cmd + + +def deactivate_operation(module, show_cmd, pkg, flag): + cmd = "install deactivate {0} forced".format(pkg) + config_cmd_operation(module, cmd) + validate_operation(module, show_cmd, cmd, pkg, flag) + return cmd + + +def terminal_operation(module, config): + if config: + cmd = "terminal dont-ask" + else: + cmd = "no terminal dont-ask" + config_cmd_operation(module, cmd) + return cmd + + +def remove_operation(module, show_cmd, pkg): + commands = [] + commands.append(terminal_operation(module, True)) + cmd = "install remove {0} forced".format(pkg) + config_cmd_operation(module, cmd) + validate_operation(module, show_cmd, cmd, pkg, True) + commands.append(cmd) + commands.append(terminal_operation(module, False)) + return commands + + +def install_remove_rpm(module, full_pkg, file_system, state): + commands = [] + reload_patch = False + + splitted_pkg = full_pkg.split(".") + pkg = ".".join(splitted_pkg[0:-1]) + + show_inactive = "show install inactive" + show_active = "show install active" + show_commit = "show install committed" + show_patches = "show install patches" + show_pkg_info = "show install pkg-info {0}".format(pkg) + + if state == "present": + inactive_body = execute_show_command(show_inactive, module) + active_body = execute_show_command(show_active, module) + + if pkg not in inactive_body and pkg not in active_body: + commands.append(add_operation(module, show_inactive, file_system, full_pkg, pkg)) + + patch_type_body = execute_show_command(show_pkg_info, module) + if patch_type_body and "Patch Type : reload" in patch_type_body: + # This is reload smu/patch rpm + reload_patch = True + + if pkg not in active_body: + if reload_patch: + commands.append(activate_reload(module, pkg, True)) + return commands + else: + commands.append(activate_operation(module, show_active, pkg)) + + commit_body = execute_show_command(show_commit, module) + if pkg not in commit_body: + patch_body = execute_show_command(show_patches, module) + if pkg in patch_body: + # This is smu/patch rpm + commands.append(commit_operation(module, show_commit, pkg, False)) + else: + err = 'Operation "install activate {0} forced" Failed'.format(pkg) + module.fail_json(msg=err) + + else: + commit_body = execute_show_command(show_commit, module) + active_body = execute_show_command(show_active, module) + + patch_type_body = execute_show_command(show_pkg_info, module) + if patch_type_body and "Patch Type : reload" in patch_type_body: + # This is reload smu/patch rpm + reload_patch = True + + if pkg in commit_body and pkg in active_body: + if reload_patch: + commands.append(activate_reload(module, pkg, False)) + return commands + else: + commands.append(deactivate_operation(module, show_active, pkg, True)) + commit_body = execute_show_command(show_commit, module) + if pkg in commit_body: + # This is smu/patch rpm + commands.append(commit_operation(module, show_commit, pkg, True)) + commands.extend(remove_operation(module, show_inactive, pkg)) + + elif pkg in commit_body: + # This is smu/patch rpm + commands.append(commit_operation(module, show_commit, pkg, True)) + commands.extend(remove_operation(module, show_inactive, pkg)) + + elif pkg in active_body: + # This is smu/patch rpm + if reload_patch: + commands.append(activate_reload(module, pkg, False)) + return commands + else: + commands.append(deactivate_operation(module, show_inactive, pkg, False)) + commands.extend(remove_operation(module, show_inactive, pkg)) + + else: + inactive_body = execute_show_command(show_inactive, module) + if pkg in inactive_body: + commands.extend(remove_operation(module, show_inactive, pkg)) + + return commands + + +def main(): + element_spec = dict( + pkg=dict(type="str"), + file_system=dict(type="str", default="bootflash"), + state=dict(choices=["absent", "present"], default="present"), + ) + + aggregate_spec = deepcopy(element_spec) + aggregate_spec["pkg"] = dict(required=True) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict(aggregate=dict(type="list", elements="dict", options=aggregate_spec)) + + argument_spec.update(element_spec) + required_one_of = [["pkg", "aggregate"]] + mutually_exclusive = [["pkg", "aggregate"]] + + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=False, + ) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + aggregate = module.params.get("aggregate") + objects = [] + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + d = item.copy() + objects.append(d) + else: + objects.append( + { + "pkg": module.params["pkg"], + "file_system": module.params["file_system"], + "state": module.params["state"], + }, + ) + + for obj in objects: + if obj["state"] == "present": + remote_exists = remote_file_exists(module, obj["pkg"], file_system=obj["file_system"]) + + if not remote_exists: + module.fail_json(msg="The requested package doesn't exist on the device") + + cmds = install_remove_rpm(module, obj["pkg"], obj["file_system"], obj["state"]) + + if cmds: + results["changed"] = True + results["commands"].extend(cmds) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snapshot.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snapshot.py new file mode 100644 index 00000000..9697a128 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snapshot.py @@ -0,0 +1,413 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + + +DOCUMENTATION = """ +module: nxos_snapshot +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manage snapshots of the running states of selected features. +description: +- Create snapshots of the running states of selected features, add new show commands + for snapshot creation, delete and compare existing snapshots. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- C(transport=cli) may cause timeout errors. +- The C(element_key1) and C(element_key2) parameter specify the tags used to distinguish + among row entries. In most cases, only the element_key1 parameter needs to specified + to be able to distinguish among row entries. +- C(action=compare) will always store a comparison report on a local file. +options: + action: + description: + - Define what snapshot action the module would perform. + required: true + choices: + - add + - compare + - create + - delete + - delete_all + type: str + snapshot_name: + description: + - Snapshot name, to be used when C(action=create) or C(action=delete). + type: str + description: + description: + - Snapshot description to be used when C(action=create). + type: str + snapshot1: + description: + - First snapshot to be used when C(action=compare). + type: str + snapshot2: + description: + - Second snapshot to be used when C(action=compare). + type: str + comparison_results_file: + description: + - Name of the file where snapshots comparison will be stored when C(action=compare). + type: str + compare_option: + description: + - Snapshot options to be used when C(action=compare). + choices: + - summary + - ipv4routes + - ipv6routes + type: str + section: + description: + - Used to name the show command output, to be used when C(action=add). + type: str + show_command: + description: + - Specify a new show command, to be used when C(action=add). + type: str + row_id: + description: + - Specifies the tag of each row entry of the show command's XML output, to be + used when C(action=add). + type: str + element_key1: + description: + - Specify the tags used to distinguish among row entries, to be used when C(action=add). + type: str + element_key2: + description: + - Specify the tags used to distinguish among row entries, to be used when C(action=add). + type: str + save_snapshot_locally: + description: + - Specify to locally store a new created snapshot, to be used when C(action=create). + type: bool + default: no + path: + description: + - Specify the path of the file where new created snapshot or snapshots comparison + will be stored, to be used when C(action=create) and C(save_snapshot_locally=true) + or C(action=compare). + default: ./ + type: str +""" + +EXAMPLES = """ +# Create a snapshot and store it locally +- cisco.nxos.nxos_snapshot: + action: create + snapshot_name: test_snapshot + description: Done with Ansible + save_snapshot_locally: true + path: /home/user/snapshots/ + +# Delete a snapshot +- cisco.nxos.nxos_snapshot: + action: delete + snapshot_name: test_snapshot + +# Delete all existing snapshots +- cisco.nxos.nxos_snapshot: + action: delete_all + +# Add a show command for snapshots creation +- cisco.nxos.nxos_snapshot: + section: myshow + show_command: show ip interface brief + row_id: ROW_intf + element_key1: intf-name + +# Compare two snapshots +- cisco.nxos.nxos_snapshot: + action: compare + snapshot1: pre_snapshot + snapshot2: post_snapshot + comparison_results_file: compare_snapshots.txt + compare_option: summary + path: ../snapshot_reports/ +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: verbose mode + type: list + sample: ["snapshot create post_snapshot Post-snapshot"] +""" + +import os +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + command = [{"command": command, "output": "text"}] + + return run_commands(module, command) + + +def get_existing(module): + existing = [] + command = "show snapshots" + + body = execute_show_command(command, module)[0] + if body: + split_body = body.splitlines() + snapshot_regex = ( + r"(?P<name>\S+)\s+(?P<date>\w+\s+\w+\s+\d+\s+\d+" + r":\d+:\d+\s+\d+)\s+(?P<description>.*)" + ) + for snapshot in split_body: + temp = {} + try: + match_snapshot = re.match(snapshot_regex, snapshot, re.DOTALL) + snapshot_group = match_snapshot.groupdict() + temp["name"] = snapshot_group["name"] + temp["date"] = snapshot_group["date"] + temp["description"] = snapshot_group["description"] + existing.append(temp) + except AttributeError: + pass + + return existing + + +def action_create(module, existing_snapshots): + commands = list() + exist = False + for snapshot in existing_snapshots: + if module.params["snapshot_name"] == snapshot["name"]: + exist = True + + if exist is False: + commands.append( + "snapshot create {0} {1}".format( + module.params["snapshot_name"], + module.params["description"], + ), + ) + + return commands + + +def action_add(module, existing_snapshots): + commands = list() + command = "show snapshot sections" + sections = [] + body = execute_show_command(command, module)[0] + + if body: + section_regex = r".*\[(?P<section>\S+)\].*" + split_body = body.split("\n\n") + for section in split_body: + temp = {} + for line in section.splitlines(): + try: + match_section = re.match(section_regex, section, re.DOTALL) + temp["section"] = match_section.groupdict()["section"] + except (AttributeError, KeyError): + pass + + if "show command" in line: + temp["show_command"] = line.split("show command: ")[1] + elif "row id" in line: + temp["row_id"] = line.split("row id: ")[1] + elif "key1" in line: + temp["element_key1"] = line.split("key1: ")[1] + elif "key2" in line: + temp["element_key2"] = line.split("key2: ")[1] + + if temp: + sections.append(temp) + + proposed = { + "section": module.params["section"], + "show_command": module.params["show_command"], + "row_id": module.params["row_id"], + "element_key1": module.params["element_key1"], + "element_key2": module.params["element_key2"] or "-", + } + + if proposed not in sections: + if module.params["element_key2"]: + commands.append( + 'snapshot section add {0} "{1}" {2} {3} {4}'.format( + module.params["section"], + module.params["show_command"], + module.params["row_id"], + module.params["element_key1"], + module.params["element_key2"], + ), + ) + else: + commands.append( + 'snapshot section add {0} "{1}" {2} {3}'.format( + module.params["section"], + module.params["show_command"], + module.params["row_id"], + module.params["element_key1"], + ), + ) + + return commands + + +def action_compare(module, existing_snapshots): + command = "show snapshot compare {0} {1}".format( + module.params["snapshot1"], + module.params["snapshot2"], + ) + + if module.params["compare_option"]: + command += " {0}".format(module.params["compare_option"]) + + body = execute_show_command(command, module)[0] + return body + + +def action_delete(module, existing_snapshots): + commands = list() + + exist = False + for snapshot in existing_snapshots: + if module.params["snapshot_name"] == snapshot["name"]: + exist = True + + if exist: + commands.append("snapshot delete {0}".format(module.params["snapshot_name"])) + + return commands + + +def action_delete_all(module, existing_snapshots): + commands = list() + if existing_snapshots: + commands.append("snapshot delete all") + return commands + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def write_on_file(content, filename, module): + path = module.params["path"] + if path[-1] != "/": + path += "/" + filepath = "{0}{1}".format(path, filename) + try: + report = open(filepath, "w") + report.write(content) + report.close() + except Exception: + module.fail_json(msg="Error while writing on file.") + + return filepath + + +def main(): + argument_spec = dict( + action=dict( + required=True, + choices=["create", "add", "compare", "delete", "delete_all"], + ), + snapshot_name=dict(type="str"), + description=dict(type="str"), + snapshot1=dict(type="str"), + snapshot2=dict(type="str"), + compare_option=dict(choices=["summary", "ipv4routes", "ipv6routes"]), + comparison_results_file=dict(type="str"), + section=dict(type="str"), + show_command=dict(type="str"), + row_id=dict(type="str"), + element_key1=dict(type="str", no_log=False), + element_key2=dict(type="str", no_log=False), + save_snapshot_locally=dict(type="bool", default=False), + path=dict(type="str", default="./"), + ) + + required_if = [ + ( + "action", + "compare", + ["snapshot1", "snapshot2", "comparison_results_file"], + ), + ("action", "create", ["snapshot_name", "description"]), + ( + "action", + "add", + ["section", "show_command", "row_id", "element_key1"], + ), + ("action", "delete", ["snapshot_name"]), + ] + + module = AnsibleModule( + argument_spec=argument_spec, + required_if=required_if, + supports_check_mode=True, + ) + + action = module.params["action"] + comparison_results_file = module.params["comparison_results_file"] + + if not os.path.isdir(module.params["path"]): + module.fail_json(msg="{0} is not a valid directory name.".format(module.params["path"])) + + existing_snapshots = invoke("get_existing", module) + action_results = invoke("action_%s" % action, module, existing_snapshots) + + result = {"changed": False, "commands": []} + + if not module.check_mode: + if action == "compare": + result["commands"] = [] + + if module.params["path"] and comparison_results_file: + snapshot1 = module.params["snapshot1"] + snapshot2 = module.params["snapshot2"] + compare_option = module.params["compare_option"] + command = "show snapshot compare {0} {1}".format(snapshot1, snapshot2) + if compare_option: + command += " {0}".format(compare_option) + content = execute_show_command(command, module)[0] + if content: + write_on_file(content, comparison_results_file, module) + else: + if action_results: + load_config(module, action_results) + result["commands"] = action_results + result["changed"] = True + + if ( + action == "create" + and module.params["path"] + and module.params["save_snapshot_locally"] + ): + command = "show snapshot dump {0} | json".format(module.params["snapshot_name"]) + content = execute_show_command(command, module)[0] + if content: + write_on_file(str(content), module.params["snapshot_name"], module) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_community.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_community.py new file mode 100644 index 00000000..4e94b1f0 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_community.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_community +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP community configs. +description: +- Manages SNMP community configuration. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +options: + community: + description: + - Case-sensitive community string. + required: true + type: str + access: + description: + - Access type for community. + choices: + - ro + - rw + type: str + group: + description: + - Group to which the community belongs. + type: str + acl: + description: + - ACL name to filter snmp requests or keyword 'default'. + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# ensure snmp community is configured +- cisco.nxos.nxos_snmp_community: + community: TESTING7 + group: network-operator + state: present +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server community TESTING7 group network-operator"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + if "show run" not in command: + output = "json" + else: + output = "text" + cmds = [{"command": command, "output": output}] + + body = run_commands(module, cmds) + return body + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_groups(module): + data = execute_show_command("show snmp group", module)[0] + group_list = [] + + try: + group_table = data["TABLE_role"]["ROW_role"] + for group in group_table: + group_list.append(group["role_name"]) + except (KeyError, AttributeError): + pass + + return group_list + + +def get_snmp_community(module, name): + command = "show run snmp all | grep word-exp {0}".format(name) + data = execute_show_command(command, module)[0] + community_dict = {} + + if not data: + return community_dict + + community_re = r"snmp-server community (\S+)" + mo = re.search(community_re, data) + if mo: + community_name = mo.group(1) + else: + return community_dict + + community_dict["group"] = None + group_re = r"snmp-server community {0} group (\S+)".format(community_name) + mo = re.search(group_re, data) + if mo: + community_dict["group"] = mo.group(1) + + community_dict["acl"] = None + acl_re = r"snmp-server community {0} use-acl (\S+)".format(community_name) + mo = re.search(acl_re, data) + if mo: + community_dict["acl"] = mo.group(1) + + return community_dict + + +def config_snmp_community(delta, community): + CMDS = { + "group": "snmp-server community {0} group {group}", + "acl": "snmp-server community {0} use-acl {acl}", + "no_acl": "no snmp-server community {0} use-acl {no_acl}", + } + commands = [] + for k in delta.keys(): + cmd = CMDS.get(k).format(community, **delta) + if cmd: + if "group" in cmd: + commands.insert(0, cmd) + else: + commands.append(cmd) + cmd = None + return commands + + +def main(): + argument_spec = dict( + community=dict(required=True, type="str"), + access=dict(choices=["ro", "rw"]), + group=dict(type="str"), + acl=dict(type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=[["access", "group"]], + mutually_exclusive=[["access", "group"]], + supports_check_mode=True, + ) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + access = module.params["access"] + group = module.params["group"] + community = module.params["community"] + acl = module.params["acl"] + state = module.params["state"] + + if access: + if access == "ro": + group = "network-operator" + elif access == "rw": + group = "network-admin" + + # group check - ensure group being configured exists on the device + configured_groups = get_snmp_groups(module) + + if group not in configured_groups: + module.fail_json(msg="Group not on switch. Please add before moving forward") + + existing = get_snmp_community(module, community) + args = dict(group=group, acl=acl) + proposed = dict((k, v) for k, v in args.items() if v is not None) + delta = dict(set(proposed.items()).difference(existing.items())) + if delta.get("acl") == "default": + delta.pop("acl") + if existing.get("acl"): + delta["no_acl"] = existing.get("acl") + + commands = [] + + if state == "absent": + if existing: + command = "no snmp-server community {0}".format(community) + commands.append(command) + elif state == "present": + if delta: + command = config_snmp_community(dict(delta), community) + commands.append(command) + + cmds = flatten_list(commands) + + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_contact.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_contact.py new file mode 100644 index 00000000..49dbc8a8 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_contact.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_contact +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP contact info. +description: +- Manages SNMP contact information. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- C(state=absent) removes the contact configuration if it is configured. +options: + contact: + description: + - Contact information. + required: true + type: str + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# ensure snmp contact is configured +- cisco.nxos.nxos_snmp_contact: + contact: Test + state: present +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server contact New_Test"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + command = {"command": command, "output": "text"} + + return run_commands(module, command) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_contact(module): + contact = {} + contact_regex = r"^\s*snmp-server\scontact\s(?P<contact>.+)$" + + body = execute_show_command("show run snmp", module)[0] + match_contact = re.search(contact_regex, body, re.M) + if match_contact: + contact["contact"] = match_contact.group("contact") + + return contact + + +def main(): + argument_spec = dict( + contact=dict(required=True, type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + contact = module.params["contact"] + state = module.params["state"] + + existing = get_snmp_contact(module) + commands = [] + + if state == "absent": + if existing and existing["contact"] == contact: + commands.append("no snmp-server contact") + elif state == "present": + if not existing or existing["contact"] != contact: + commands.append("snmp-server contact {0}".format(contact)) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_host.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_host.py new file mode 100644 index 00000000..3c23374c --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_host.py @@ -0,0 +1,513 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_host +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP host configuration. +description: +- Manages SNMP host configuration parameters. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- C(state=absent) removes the host configuration if it is configured. +options: + snmp_host: + description: + - IP address of hostname of target host. + required: true + type: str + version: + description: + - SNMP version. If this is not specified, v1 is used. + choices: + - v1 + - v2c + - v3 + type: str + v3: + description: + - Use this when verion is v3. SNMPv3 Security level. + choices: + - noauth + - auth + - priv + type: str + community: + description: + - Community string or v3 username. + type: str + udp: + description: + - UDP port number (0-65535). + default: '162' + type: str + snmp_type: + description: + - type of message to send to host. If this is not specified, trap type is used. + choices: + - trap + - inform + type: str + vrf: + description: + - VRF to use to source traffic to source. If state = absent, the vrf is removed. + type: str + vrf_filter: + description: + - Name of VRF to filter. If state = absent, the vrf is removed from the filter. + type: str + src_intf: + description: + - Source interface. Must be fully qualified interface name. If state = absent, + the interface is removed. + type: str + state: + description: + - Manage the state of the resource. If state = present, the host is added to the + configuration. If only vrf and/or vrf_filter and/or src_intf are given, they + will be added to the existing host configuration. If state = absent, the host + is removed if community parameter is given. It is possible to remove only vrf + and/or src_int and/or vrf_filter by providing only those parameters and no community + parameter. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# ensure snmp host is configured +- cisco.nxos.nxos_snmp_host: + snmp_host: 192.0.2.3 + community: TESTING + state: present +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server host 192.0.2.3 filter-vrf another_test_vrf"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + command = {"command": command, "output": "json"} + + return run_commands(module, command) + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = str(value) + else: + new_dict[new_key] = value + return new_dict + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_host(host, udp, module): + body = execute_show_command("show snmp host", module) + + host_map = { + "port": "udp", + "version": "version", + "level": "v3", + "type": "snmp_type", + "secname": "community", + } + + host_map_5k = { + "port": "udp", + "version": "version", + "sec_level": "v3", + "notif_type": "snmp_type", + "commun_or_user": "community", + } + + resource = {} + + if body: + try: + resource_table = body[0]["TABLE_host"]["ROW_host"] + + if isinstance(resource_table, dict): + resource_table = [resource_table] + + for each in resource_table: + key = str(each["host"]) + "_" + str(each["port"]).strip() + src = each.get("src_intf") + host_resource = apply_key_map(host_map, each) + + if src: + host_resource["src_intf"] = src + if re.search(r"interface:", src): + host_resource["src_intf"] = src.split(":")[1].strip() + + vrf_filt = each.get("TABLE_vrf_filters") + if vrf_filt: + vrf_filter = vrf_filt["ROW_vrf_filters"]["vrf_filter"].split(":")[1].split(",") + filters = [vrf.strip() for vrf in vrf_filter] + host_resource["vrf_filter"] = filters + + vrf = each.get("vrf") + if vrf: + host_resource["vrf"] = vrf.split(":")[1].strip() + resource[key] = host_resource + except KeyError: + # Handle the 5K case + try: + resource_table = body[0]["TABLE_hosts"]["ROW_hosts"] + + if isinstance(resource_table, dict): + resource_table = [resource_table] + + for each in resource_table: + key = str(each["address"]) + "_" + str(each["port"]).strip() + src = each.get("src_intf") + host_resource = apply_key_map(host_map_5k, each) + + if src: + host_resource["src_intf"] = src + if re.search(r"interface:", src): + host_resource["src_intf"] = src.split(":")[1].strip() + + vrf = each.get("use_vrf_name") + if vrf: + host_resource["vrf"] = vrf.strip() + + vrf_filt = each.get("TABLE_filter_vrf") + if vrf_filt: + vrf_filter = vrf_filt["ROW_filter_vrf"]["filter_vrf_name"].split(",") + filters = [vrf.strip() for vrf in vrf_filter] + host_resource["vrf_filter"] = filters + + resource[key] = host_resource + except (KeyError, AttributeError, TypeError): + return resource + except (AttributeError, TypeError): + return resource + + find = resource.get(host + "_" + udp) + + if find: + fix_find = {} + for key, value in find.items(): + if isinstance(value, str): + fix_find[key] = value.strip() + else: + fix_find[key] = value + return fix_find + + return {} + + +def remove_snmp_host(host, udp, existing): + commands = [] + if existing["version"] == "v3": + existing["version"] = "3" + command = "no snmp-server host {0} {snmp_type} version \ + {version} {v3} {community} udp-port {1}".format( + host, udp, **existing + ) + + elif existing["version"] == "v2c": + existing["version"] = "2c" + command = "no snmp-server host {0} {snmp_type} version \ + {version} {community} udp-port {1}".format( + host, udp, **existing + ) + + elif existing["version"] == "v1": + existing["version"] = "1" + command = "no snmp-server host {0} {snmp_type} version \ + {version} {community} udp-port {1}".format( + host, udp, **existing + ) + + if command: + commands.append(command) + return commands + + +def remove_vrf(host, udp, proposed, existing): + commands = [] + if existing.get("vrf"): + commands.append( + "no snmp-server host {0} use-vrf \ + {1} udp-port {2}".format( + host, + proposed.get("vrf"), + udp, + ), + ) + return commands + + +def remove_filter(host, udp, proposed, existing): + commands = [] + if existing.get("vrf_filter"): + if proposed.get("vrf_filter") in existing.get("vrf_filter"): + commands.append( + "no snmp-server host {0} filter-vrf \ + {1} udp-port {2}".format( + host, + proposed.get("vrf_filter"), + udp, + ), + ) + return commands + + +def remove_src(host, udp, proposed, existing): + commands = [] + if existing.get("src_intf"): + commands.append( + "no snmp-server host {0} source-interface \ + {1} udp-port {2}".format( + host, + proposed.get("src_intf"), + udp, + ), + ) + return commands + + +def config_snmp_host(delta, udp, proposed, existing, module): + commands = [] + command_builder = [] + host = proposed["snmp_host"] + cmd = "snmp-server host {0}".format(proposed["snmp_host"]) + + snmp_type = delta.get("snmp_type") + version = delta.get("version") + ver = delta.get("v3") + community = delta.get("community") + + command_builder.append(cmd) + if any([snmp_type, version, ver, community]): + type_string = snmp_type or existing.get("type") + if type_string: + command_builder.append(type_string) + + version = version or existing.get("version") + if version: + if version == "v1": + vn = "1" + elif version == "v2c": + vn = "2c" + elif version == "v3": + vn = "3" + + version_string = "version {0}".format(vn) + command_builder.append(version_string) + + if ver: + ver_string = ver or existing.get("v3") + command_builder.append(ver_string) + + if community: + community_string = community or existing.get("community") + command_builder.append(community_string) + + udp_string = " udp-port {0}".format(udp) + command_builder.append(udp_string) + + cmd = " ".join(command_builder) + + commands.append(cmd) + + CMDS = { + "vrf_filter": "snmp-server host {0} filter-vrf {vrf_filter} udp-port {1}", + "vrf": "snmp-server host {0} use-vrf {vrf} udp-port {1}", + "src_intf": "snmp-server host {0} source-interface {src_intf} udp-port {1}", + } + + for key in delta: + command = CMDS.get(key) + if command: + cmd = command.format(host, udp, **delta) + commands.append(cmd) + return commands + + +def main(): + argument_spec = dict( + snmp_host=dict(required=True, type="str"), + community=dict(type="str"), + udp=dict(type="str", default="162"), + version=dict(choices=["v1", "v2c", "v3"]), + src_intf=dict(type="str"), + v3=dict(choices=["noauth", "auth", "priv"]), + vrf_filter=dict(type="str"), + vrf=dict(type="str"), + snmp_type=dict(choices=["trap", "inform"]), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + snmp_host = module.params["snmp_host"] + community = module.params["community"] + udp = module.params["udp"] + version = module.params["version"] + src_intf = module.params["src_intf"] + v3 = module.params["v3"] + vrf_filter = module.params["vrf_filter"] + vrf = module.params["vrf"] + snmp_type = module.params["snmp_type"] + state = module.params["state"] + + existing = get_snmp_host(snmp_host, udp, module) + + if version is None: + if existing: + version = existing.get("version") + else: + version = "v1" + + if snmp_type is None: + if existing: + snmp_type = existing.get("snmp_type") + else: + snmp_type = "trap" + + if v3 is None: + if version == "v3" and existing: + v3 = existing.get("v3") + + if snmp_type == "inform" and version == "v1": + module.fail_json(msg="inform requires snmp v2c or v3") + + if (version == "v1" or version == "v2c") and v3: + module.fail_json(msg='param: "v3" should not be used when ' "using version v1 or v2c") + + if not any([vrf_filter, vrf, src_intf]): + if not all([snmp_type, version, community, udp]): + module.fail_json( + msg="when not configuring options like " + "vrf_filter, vrf, and src_intf," + "the following params are required: " + "type, version, community", + ) + + if version == "v3" and v3 is None: + module.fail_json( + msg="when using version=v3, the param v3 " + "(options: auth, noauth, priv) is also required", + ) + + # existing returns the list of vrfs configured for a given host + # checking to see if the proposed is in the list + store = existing.get("vrf_filter") + if existing and store: + if vrf_filter not in existing["vrf_filter"]: + existing["vrf_filter"] = None + else: + existing["vrf_filter"] = vrf_filter + commands = [] + + args = dict( + community=community, + snmp_host=snmp_host, + udp=udp, + version=version, + src_intf=src_intf, + vrf_filter=vrf_filter, + v3=v3, + vrf=vrf, + snmp_type=snmp_type, + ) + proposed = dict((k, v) for k, v in args.items() if v is not None) + + if state == "absent" and existing: + if proposed.get("community"): + commands.append(remove_snmp_host(snmp_host, udp, existing)) + else: + if proposed.get("src_intf"): + commands.append(remove_src(snmp_host, udp, proposed, existing)) + if proposed.get("vrf"): + commands.append(remove_vrf(snmp_host, udp, proposed, existing)) + if proposed.get("vrf_filter"): + commands.append(remove_filter(snmp_host, udp, proposed, existing)) + + elif state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = config_snmp_host(delta, udp, proposed, existing, module) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_location.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_location.py new file mode 100644 index 00000000..d7526a9e --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_location.py @@ -0,0 +1,156 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_location +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP location information. +description: +- Manages SNMP location configuration. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +options: + location: + description: + - Location information. + required: true + type: str + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# ensure snmp location is configured +- cisco.nxos.nxos_snmp_location: + location: Test + state: present + +# ensure snmp location is not configured +- cisco.nxos.nxos_snmp_location: + location: Test + state: absent +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server location New_Test"] +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module): + command = {"command": command, "output": "text"} + + return run_commands(module, command) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_location(module): + location = {} + location_regex = r"^\s*snmp-server\s+location\s+(?P<location>.+)$" + + body = execute_show_command("show run snmp", module)[0] + match_location = re.search(location_regex, body, re.M) + if match_location: + location["location"] = match_location.group("location") + + return location + + +def main(): + argument_spec = dict( + location=dict(required=True, type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + location = module.params["location"] + state = module.params["state"] + + existing = get_snmp_location(module) + commands = [] + + if state == "absent": + if existing and existing["location"] == location: + commands.append("no snmp-server location") + elif state == "present": + if not existing or existing["location"] != location: + commands.append("snmp-server location {0}".format(location)) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_server.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_server.py new file mode 100644 index 00000000..7354dc1e --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_server.py @@ -0,0 +1,1480 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +""" +The module file for nxos_snmp_server +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + +DOCUMENTATION = """ +module: nxos_snmp_server +short_description: SNMP Server resource module. +description: +- This module manages SNMP server configuration on devices running Cisco NX-OS. +version_added: 2.8.0 +notes: +- Tested against NX-OS 9.3.6 on Cisco Nexus Switches. +- This module works with connection C(network_cli) and C(httpapi). +- Tested against Cisco MDS NX-OS 9.2(2) with connection C(network_cli). +author: Nilashish Chakraborty (@NilashishC) +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the command B(show running-config | section '^snmp-server'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dict of SNMP server configuration + type: dict + suboptions: + aaa_user: + description: Set duration for which aaa-cached snmp user exists. + type: dict + suboptions: + cache_timeout: + description: Timeout for which aaa-cached user exists(in secs). + type: int + communities: + description: Set community string and access privs. + type: list + elements: dict + suboptions: + name: + description: SNMP community string (Max Size 32). + type: str + aliases: ["community"] + group: + description: Group to which the community belongs. + type: str + ro: + description: Read-only access with this community string. + type: bool + rw: + description: Read-write access with this community string. + type: bool + use_ipv4acl: + description: + - Specify IPv4 ACL, the ACL name specified must be IPv4 ACL. + - This option is unsupported on MDS switches. + type: str + use_ipv6acl: + description: + - Specify IPv6 ACL, the ACL name specified after must be IPv6 ACL. + - This option is unsupported on MDS switches. + type: str + contact: + description: Modify sysContact. + type: str + context: + description: SNMP context to be mapped. + type: dict + suboptions: + name: + description: Name of the SNMP context (Max Size 32). + type: str + instance: + description: Name of the protocol instance (Max Size 32). + type: str + topology: + description: Topology associated with the SNMP context. + type: str + vrf: + description: + - VRF associated with the SNMP context. + - This option is unsupported on MDS switches. + type: str + counter: + description: + - Configure port counter configuration. + - This option is unsupported on MDS switches. + type: dict + suboptions: + cache: + description: Port stats cache. + type: dict + suboptions: + enable: + description: Enable port stats cache. + type: bool + timeout: + description: Timeout for which cached port stats exists(in secs). + type: int + drop: + description: + - Silently drop unknown v3 user packets. + - This option is unsupported on MDS switches. + type: dict + suboptions: + unknown_engine_id: + description: Unknown v3 engine id. + type: bool + unknown_user: + description: Unknown v3 user. + type: bool + traps: + description: Enable SNMP Traps + type: dict + suboptions: + aaa: + description: AAA traps + type: dict + suboptions: + enable: + description: Enable AAA traps. + type: bool + server_state_change: + description: AAA server state change notification. + type: bool + bgp: + description: SNMP BGP traps. + type: dict + suboptions: + enable: + description: Enable SNMP BGP traps. + type: bool + bridge: + description: + - Bridge traps. + - This option is unsupported on MDS switches. + type: dict + suboptions: + enable: + description: Enable bridge traps. + type: bool + newroot: + description: Enable SNMP STP Bridge MIB newroot traps. + type: bool + topologychange: + description: Enable SNMP STP Bridge MIB topologychange traps. + type: bool + callhome: + description: Callhome traps. + type: dict + suboptions: + enable: + description: + - Enable callhome traps. + - This option is unsupported on MDS switches. + type: bool + event_notify: + description: Callhome External Event Notification. + type: bool + smtp_send_fail: + description: SMTP Message Send Fail notification. + type: bool + cfs: + description: CFS traps. + type: dict + suboptions: + enable: + description: + - Enable cfs traps. + - This option is unsupported on MDS switches. + type: bool + merge_failure: + description: Merge failure notification. + type: bool + state_change_notif: + description: State change notification. + type: bool + config: + description: Config traps. + type: dict + suboptions: + enable: + description: + - Enable config traps. + - This option is unsupported on MDS switches. + type: bool + ccmCLIRunningConfigChanged: + description: Running config change trap. + type: bool + entity: + description: Entity traps. + type: dict + suboptions: + enable: + description: Enable entity traps. + type: bool + cefcMIBEnableStatusNotification: + description: CefcMIBEnableStatusNotification. + type: bool + entity_fan_status_change: + description: Entity Fan Status Change. + type: bool + entity_mib_change: + description: Entity MIB change. + type: bool + entity_module_inserted: + description: Entity Module Inserted. + type: bool + entity_module_removed: + description: Entity Module Removed. + type: bool + entity_module_status_change: + description: Entity Module Status Change. + type: bool + entity_power_out_change: + description: Entity Power Out Change. + type: bool + entity_power_status_change: + description: Entity Power Status Change. + type: bool + entity_sensor: + description: Entity sensor. + type: bool + entity_unrecognised_module: + description: Entity Unrecognised Module. + type: bool + feature_control: + description: Feature-Control traps. + type: dict + suboptions: + enable: + description: + - Enable feature-control traps. + - This option is unsupported on MDS switches. + type: bool + featureOpStatusChange: + description: Feature operation status change notification. + type: bool + ciscoFeatOpStatusChange: + description: Feature operation status change Notification. + type: bool + generic: + description: Generic traps. + type: dict + suboptions: + enable: + description: + - Enable generic traps. + - This option is unsupported on MDS switches. + type: bool + coldStart: + description: Generic coldStart trap. + type: bool + warmStart: + description: Generic warmStart trap. + type: bool + license: + description: License traps. + type: dict + suboptions: + enable: + description: + - Enable license traps. + - This option is unsupported on MDS switches. + type: bool + notify_license_expiry: + description: License Expiry Notification. + type: bool + notify_license_expiry_warning: + description: License Expiry Warning Notification. + type: bool + notify_licensefile_missing: + description: License File Missing Notification. + type: bool + notify_no_license_for_feature: + description: No License installed for feature Notification. + type: bool + link: + description: Link traps. + type: dict + suboptions: + enable: + description: + - Enable link traps. + - This option is unsupported on MDS switches. + type: bool + cErrDisableInterfaceEventRev1: + description: + - Err-disable state notification. + - This option is unsupported on MDS switches. + type: bool + cieLinkDown: + description: Cisco extended link state down notification. + type: bool + cieLinkUp: + description: Cisco extended link state up notification. + type: bool + cisco_xcvr_mon_status_chg: + description: Cisco interface transceiver monitor status change notification. + type: bool + cmn_mac_move_notification: + description: + - Mac addr move trap. + - This option is unsupported on MDS switches. + type: bool + delayed_link_state_change: + description: Delayed link state change. + type: bool + extended_linkDown: + description: IETF extended link state down notification. + type: bool + extended_linkUp: + description: IETF extended link state up notification. + type: bool + linkDown: + description: IETF Link state down notification. + type: bool + linkUp: + description: IETF Link state up notification. + type: bool + mmode: + description: + - MMode traps. + - This option is unsupported on MDS switches. + type: dict + suboptions: + enable: + description: Enable mmode traps. + type: bool + cseMaintModeChangeNotify: + description: Maint Mode Change Notification. + type: bool + cseNormalModeChangeNotify: + description: Normal Mode Change Notification. + type: bool + ospf: + description: SNMP OSPF traps. + type: dict + suboptions: + enable: + description: Enable SNMP OSPF traps. + type: bool + ospfv3: + description: SNMP OSPFv3 traps. + type: dict + suboptions: + enable: + description: Enable SNMP OSPFv3 traps. + type: bool + rf: + description: RF traps. + type: dict + suboptions: + enable: + description: + - Enable rf traps. + - This option is unsupported on MDS switches. + type: bool + redundancy_framework: + description: Redundancy_Framework (RF) Sup switchover MIB. + type: bool + rmon: + description: RMON traps. + type: dict + suboptions: + enable: + description: + - Enable rmon traps. + - This option is unsupported on MDS switches. + type: bool + fallingAlarm: + description: Rmon falling alarm. + type: bool + hcFallingAlarm: + description: High capacity Rmon falling alarm. + type: bool + hcRisingAlarm: + description: High capacity Rmon rising alarm. + type: bool + risingAlarm: + description: Rmon rising alarm. + type: bool + snmp: + description: SNMP traps. + type: dict + suboptions: + enable: + description: + - Enable snmp traps. + - This option is unsupported on MDS switches. + type: bool + authentication: + description: SNMP authentication trap. + type: bool + storm_control: + description: Storm-Control traps. + type: dict + suboptions: + enable: + description: + - Enable storm-control traps. + - This option is unsupported on MDS switches. + type: bool + cpscEventRev1: + description: + - Port-Storm-Control-Event. + - This option is unsupported on MDS switches. + type: bool + trap_rate: + description: Number of traps per minute. + type: bool + stpx: + description: + - Stpx traps. + - This option is unsupported on MDS switches. + type: dict + suboptions: + enable: + description: Enable stpx traps. + type: bool + inconsistency: + description: Enable SNMP STPX MIB InconsistencyUpdate traps. + type: bool + loop_inconsistency: + description: Enable SNMP STPX MIB LoopInconsistencyUpdate traps. + type: bool + root_inconsistency: + description: Enable SNMP STPX MIB RootInconsistencyUpdate traps. + type: bool + syslog: + description: Enable syslog traps. + type: dict + suboptions: + enable: + description: + - Enable syslog traps. + - This option is unsupported on MDS switches. + type: bool + message_generated: + description: Message Generated Notification. + type: bool + sysmgr: + description: Sysmgr traps. + type: dict + suboptions: + enable: + description: + - Enable sysmgr traps. + - This option is unsupported on MDS switches. + type: bool + cseFailSwCoreNotifyExtended: + description: Software Core Notification. + type: bool + system: + description: System traps. + type: dict + suboptions: + enable: + description: + - Enable system traps. + - This option is unsupported on MDS switches. + type: bool + clock_change_notification: + description: Clock-change-notification traps. + type: bool + upgrade: + description: Upgrade traps. + type: dict + suboptions: + enable: + description: + - Enable upgrade traps. + - This option is unsupported on MDS switches. + type: bool + upgradeJobStatusNotify: + description: Upgrade Job Status Notification. + type: bool + upgradeOpNotifyOnCompletion: + description: Upgrade Global Status Notification. + type: bool + vtp: + description: + - VTP traps. + - This option is unsupported on MDS switches. + type: dict + suboptions: + enable: + description: Enable VTP traps. + type: bool + notifs: + description: + - Enable vtpConfigRevNumberError vtpConfigDigestEnable vtpConfigRevNumberError vtpConfigDigestError + vtpServerDisabled vtpVersionOneDeviceDetected vlanTrunkPortDynamicStatusChange vtpLocalModeChanged + vtpVersionInUseChanged notification. + type: bool + vlancreate: + description: Enable vtpVlanCreated notification. + type: bool + vlandelete: + description: Enable vtpVlanDeleted notification. + type: bool + engine_id: + description: + - Configure a local SNMPv3 engineID. + - This option is unsupported on MDS switches. + type: dict + suboptions: + local: + description: EngineID of the local agent. + type: str + global_enforce_priv: + description: Globally enforce privacy for all the users. + type: bool + hosts: + description: + - Specify hosts to receive SNMP notifications. + - SNMP hosts config lines that appear separately in running-config must be added as individual dictionaries. + type: list + elements: dict + suboptions: + host: + description: IPv4 or IPv6 address or DNS Name of SNMP notification host. + type: str + community: + description: SNMP community string or SNMPv3 user name (Max Size 32). + type: str + filter_vrf: + description: + - Filters notifications to the notification host receiver based on the configured VRF. + - This option is unsupported on MDS switches. + type: str + informs: + description: Send Inform messages to this host. + type: bool + source_interface: + description: Source interface to be used for sending out SNMP notifications to this host. + type: str + traps: + description: Send Traps messages to this host. + type: bool + use_vrf: + description: + - Configures SNMP to use the selected VRF to communicate with the host receiver. + - This option is unsupported on MDS switches. + type: str + version: + description: SNMP version to use for notification messages. + type: str + choices: ["1", "2c", "3"] + auth: + description: Use the SNMPv3 authNoPriv Security Level. + type: str + priv: + description: Use the SNMPv3 authPriv Security Level. + type: str + udp_port: + description: The notification host's UDP port number. + type: int + location: + description: Modify sysLocation. + type: str + mib: + description: Mib access parameters. + type: dict + suboptions: + community_map: + description: SNMP community. + type: dict + suboptions: + community: + description: SNMP community string (Max Size 32). + type: str + context: + description: Name of the SNMP context (Max Size 32). + type: str + packetsize: + description: Largest SNMP packet size + type: int + protocol: + description: Snmp protocol operations. + type: dict + suboptions: + enable: + description: Enable/Disable snmp protocol operations. + type: bool + source_interface: + description: + - Source interface to be used for sending out SNMP notifications. + - This option is unsupported on MDS switches. + type: dict + suboptions: + informs: + description: SNMP Inform notifications for which this source interface needs to be used. + type: str + traps: + description: SNMP Trap notifications for which this source interface needs to be used. + type: str + system_shutdown: + description: Configure snmp-server for reload(2). + type: bool + tcp_session: + description: Enable one time authentication for snmp over tcp session. + type: dict + suboptions: + enable: + description: + - Enable tcp-session. + - This option is unsupported on MDS switches. + type: bool + auth: + description: Enable one time authentication for snmp over tcp session. + type: bool + users: + description: Define users who can access the SNMP engine. + type: dict + suboptions: + auth: + description: SNMP User authentication related settings + type: list + elements: dict + suboptions: + user: + description: Name of the user (Max Size 28). + type: str + group: + description: Group name (ignored for notif target user) (Max Size 28). + type: str + authentication: + description: Authentication parameters for the user. + type: dict + suboptions: + algorithm: + description: Select algorithm for authentication. + type: str + choices: ["md5", "sha", "sha-256"] + password: + description: + - Authentication password for user (Max Size 127). + - If this value is localized, it has to be enclosed in quotes in the task. + type: str + engine_id: + description: + - EngineID for configuring notif target user (for V3 informs). + - This value needs to be enclosed in quotes in the task. + type: str + localized_key: + description: Specifies whether the passwords are in localized key format. + type: bool + localizedv2_key: + description: Specifies whether the passwords are in localized V2 key format. + type: bool + priv: + description: Encryption parameters for the user. + type: dict + suboptions: + privacy_password: + description: + - Privacy password for user (Max Size 130). + - If this value is localized, it has to be enclosed in quotes in the task. + type: str + aes_128: + description: Use 128-bit AES algorithm for privacy. + type: bool + use_acls: + description: Set IPv4 and IPv6 ACL to use. + type: list + elements: dict + suboptions: + user: + description: Name of the user (Max Size 28). + type: str + ipv4: + description: Specify IPv4 ACL, the ACL name specified after must be IPv4 ACL. + type: str + ipv6: + description: Specify IPv6 ACL, the ACL name specified after must be IPv6 ACL. + type: str + state: + description: + - The state the configuration should be left in. + - The states C(replaced) and C(overridden) have identical behaviour for this module. + - Please refer to examples for more details. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - parsed + - gathered + - rendered + default: merged +""" + +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey + +- name: Merge the provided configuration with the existing running configuration + cisco.nxos.nxos_snmp_server: + config: + aaa_user: + cache_timeout: 36000 + communities: + - community: public + group: network-operator + - community: private + group: network-admin + contact: nxosswitchadmin@localhost + location: serverroom-1 + traps: + aaa: + server_state_change: True + system: + clock_change_notification: True + hosts: + - host: 192.0.2.1 + traps: True + version: '1' + community: public + - host: 192.0.2.1 + source_interface: Ethernet1/1 + - host: 192.0.2.2 + informs: True + version: '3' + auth: NMS + users: + auth: + - user: snmp_user_1 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + - user: snmp_user_2 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + priv: + privacy_password: '0x5632724fb8ac3699296af26281e1d0f1' + aes_128: True + use_acls: + - user: snmp_user_1 + ipv4: acl1 + ipv6: acl2 + - user: snmp_user_2 + ipv4: acl3 + ipv6: acl4 + +# Task output +# ------------- +# before: +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# +# commands: +# - snmp-server contact nxosswitchadmin@localhost +# - snmp-server location serverroom-1 +# - snmp-server aaa-user cache-timeout 36000 +# - snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# - snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# - snmp-server host 192.0.2.1 traps version 1 public +# - snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# - snmp-server host 192.0.2.2 informs version 3 auth NMS +# - snmp-server community private group network-admin +# - snmp-server community public group network-operator +# - snmp-server enable traps aaa server-state-change +# - snmp-server enable traps system Clock-change-notification +# +# after: +# aaa_user: +# cache_timeout: 36000 +# communities: +# - community: private +# group: network-admin +# - community: public +# group: network-operator +# contact: nxosswitchadmin@localhost +# location: serverroom-1 +# traps: +# aaa: +# server_state_change: True +# system: +# clock_change_notification: True +# hosts: +# - host: 192.0.2.1 +# traps: true +# version: "1" +# community: public +# +# - host: 192.0.2.1 +# source_interface: Ethernet1/1 +# +# - host: 192.0.2.2 +# informs: true +# version: "3" +# auth: NMS +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# +# - user: snmp_user_1 +# group: network-operator +# authentication: +# algorithm: md5 +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# localized_key: True +# +# - authentication: +# algorithm: md5 +# localized_key: true +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# priv: +# aes_128: true +# privacy_password: "0x5632724fb8ac3699296af26281e1d0f1" +# group: network-operator +# user: snmp_user_2 +# +# use_acls: +# - user: snmp_user_1 +# ipv4: acl1 +# ipv6: acl2 +# - user: snmp_user_2 +# ipv4: acl3 +# ipv6: acl4 + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server contact nxosswitchadmin@localhost +# snmp-server location serverroom-1 +# snmp-server aaa-user cache-timeout 36000 +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey +# snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# snmp-server host 192.0.2.1 traps version 1 public +# snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# snmp-server host 192.0.2.2 informs version 3 auth NMS +# snmp-server community private group network-admin +# snmp-server community public group network-operator +# snmp-server enable traps aaa server-state-change +# snmp-server enable traps system Clock-change-notification + +# Using replaced + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server contact nxosswitchadmin@localhost +# snmp-server location serverroom-1 +# snmp-server aaa-user cache-timeout 36000 +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey +# snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# snmp-server host 192.0.2.1 traps version 1 public +# snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# snmp-server host 192.0.2.2 informs version 3 auth NMS +# snmp-server community private group network-admin +# snmp-server community public group network-operator +# snmp-server enable traps aaa server-state-change +# snmp-server enable traps system Clock-change-notification + +- name: Replace snmp-server configurations of listed snmp-server with provided configurations + cisco.nxos.nxos_snmp_server: + config: + aaa_user: + cache_timeout: 36000 + communities: + - community: public + group: network-operator + - community: secret + group: network-operator + contact: nxosswitchadmin2@localhost + location: serverroom-2 + traps: + aaa: + server_state_change: True + hosts: + - host: 192.0.2.1 + traps: True + version: '1' + community: public + - host: 192.0.2.1 + source_interface: Ethernet1/1 + - host: 192.0.3.2 + informs: True + version: '3' + auth: NMS + users: + auth: + - user: admin + group: network-admin + authentication: + algorithm: md5 + password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" + localized_key: True + priv: + privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" + + - user: snmp_user_1 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + + - user: snmp_user_2 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + priv: + privacy_password: '0x5632724fb8ac3699296af26281e1d0f1' + aes_128: True + use_acls: + - user: snmp_user_1 + ipv4: acl1 + ipv6: acl2 + state: replaced + +# Task output +# ------------- +# before: +# aaa_user: +# cache_timeout: 36000 +# communities: +# - community: private +# group: network-admin +# - community: public +# group: network-operator +# contact: nxosswitchadmin@localhost +# location: serverroom-1 +# traps: +# aaa: +# server_state_change: True +# system: +# clock_change_notification: True +# hosts: +# - host: 192.0.2.1 +# traps: true +# version: "1" +# community: public +# +# - host: 192.0.2.1 +# source_interface: Ethernet1/1 +# +# - host: 192.0.2.2 +# informs: true +# version: "3" +# auth: NMS +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# +# - user: snmp_user_1 +# group: network-operator +# authentication: +# algorithm: md5 +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# localized_key: True +# +# - authentication: +# algorithm: md5 +# localized_key: true +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# priv: +# aes_128: true +# privacy_password: "0x5632724fb8ac3699296af26281e1d0f1" +# group: network-operator +# user: snmp_user_2 +# +# use_acls: +# - user: snmp_user_1 +# ipv4: acl1 +# ipv6: acl2 +# - user: snmp_user_2 +# ipv4: acl3 +# ipv6: acl4 +# +# commands: +# - snmp-server contact nxosswitchadmin2@localhost +# - no snmp-server enable traps system Clock-change-notification +# - snmp-server location serverroom-2 +# - no snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# - no snmp-server host 192.0.2.2 informs version 3 auth NMS +# - snmp-server host 192.0.3.2 informs version 3 auth NMS +# - no snmp-server community private group network-admin +# - snmp-server community secret group network-operator +# +# after: +# aaa_user: +# cache_timeout: 36000 +# communities: +# - community: public +# group: network-operator +# - community: secret +# group: network-operator +# contact: nxosswitchadmin2@localhost +# location: serverroom-2 +# traps: +# aaa: +# server_state_change: True +# hosts: +# - host: 192.0.2.1 +# traps: True +# version: '1' +# community: public +# - host: 192.0.2.1 +# source_interface: Ethernet1/1 +# - host: 192.0.3.2 +# informs: True +# version: '3' +# auth: NMS +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# +# - user: snmp_user_1 +# group: network-operator +# authentication: +# algorithm: md5 +# password: '0x5632724fb8ac3699296af26281e1d0f1' +# localized_key: True +# +# - user: snmp_user_2 +# group: network-operator +# authentication: +# algorithm: md5 +# password: '0x5632724fb8ac3699296af26281e1d0f1' +# localized_key: True +# priv: +# privacy_password: '0x5632724fb8ac3699296af26281e1d0f1' +# aes_128: True +# +# use_acls: +# - user: snmp_user_1 +# ipv4: acl1 +# ipv6: acl2 +# + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server contact nxosswitchadmin2@localhost +# snmp-server location serverroom-2 +# snmp-server aaa-user cache-timeout 36000 +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey +# snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# snmp-server host 192.0.2.1 traps version 1 public +# snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# snmp-server host 192.0.2.2 informs version 3 auth NMS +# snmp-server community secret group network-operator +# snmp-server community public group network-operator +# snmp-server enable traps aaa server-state-change +# snmp-server enable traps system Clock-change-notification + +# Using deleted + +# Before state: +# ------------ +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server contact nxosswitchadmin@localhost +# snmp-server location serverroom-1 +# snmp-server aaa-user cache-timeout 36000 +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey +# snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# snmp-server host 192.0.2.1 traps version 1 public +# snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# snmp-server host 192.0.2.2 informs version 3 auth NMS +# snmp-server community private group network-admin +# snmp-server community public group network-operator +# snmp-server enable traps aaa server-state-change +# snmp-server enable traps system Clock-change-notification + +- name: Delete SNMP Server configurations from the device (admin user will not be deleted) + cisco.nxos.nxos_snmp_server: + state: deleted + +# Task output +# ------------- +# before: +# aaa_user: +# cache_timeout: 36000 +# communities: +# - community: private +# group: network-admin +# - community: public +# group: network-operator +# contact: nxosswitchadmin@localhost +# location: serverroom-1 +# traps: +# aaa: +# server_state_change: True +# system: +# clock_change_notification: True +# hosts: +# - host: 192.0.2.1 +# traps: true +# version: "1" +# community: public +# +# - host: 192.0.2.1 +# source_interface: Ethernet1/1 +# +# - host: 192.0.2.2 +# informs: true +# version: "3" +# auth: NMS +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# +# - user: snmp_user_1 +# group: network-operator +# authentication: +# algorithm: md5 +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# localized_key: True +# +# - authentication: +# algorithm: md5 +# localized_key: true +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# priv: +# aes_128: true +# privacy_password: "0x5632724fb8ac3699296af26281e1d0f1" +# group: network-operator +# user: snmp_user_2 +# +# use_acls: +# - user: snmp_user_1 +# ipv4: acl1 +# ipv6: acl2 +# - user: snmp_user_2 +# ipv4: acl3 +# ipv6: acl4 +# +# commands: +# - no snmp-server contact nxosswitchadmin@localhost +# - no snmp-server location serverroom-1 +# - no snmp-server aaa-user cache-timeout 36000 +# - no snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey +# - no snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - no snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - no snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# - no snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# - no snmp-server host 192.0.2.1 traps version 1 public +# - no snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# - no snmp-server host 192.0.2.2 informs version 3 auth NMS +# - no snmp-server community private group network-admin +# - no snmp-server community public group network-operator +# - no snmp-server enable traps aaa server-state-change +# - no snmp-server enable traps system Clock-change-notification +# +# after: +# users: +# auth: +# - user: admin +# group: network-admin +# authentication: +# algorithm: md5 +# password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" +# localized_key: True +# priv: +# privacy_password: "0xcbde46b02c46e0bcd3ac5af6a8b13da9" + +# After state: +# ------------ +# nxos-9k-rdo# show running-config | section "^snmp-server" +# snmp-server user admin network-admin auth md5 0xcbde46b02c46e0bcd3ac5af6a8b13da9 priv 0xcbde46b02c46e0bcd3ac5af6a8b13da9 localizedkey + +# Using rendered +# --------------- + +- name: Render platform specific configuration lines with state rendered (without connecting to the device) + cisco.nxos.nxos_snmp_server: + config: + aaa_user: + cache_timeout: 36000 + communities: + - community: public + group: network-operator + - community: private + group: network-admin + contact: nxosswitchadmin@localhost + location: serverroom-1 + traps: + aaa: + server_state_change: True + system: + clock_change_notification: True + hosts: + - host: 192.0.2.1 + traps: True + version: '1' + community: public + - host: 192.0.2.1 + source_interface: Ethernet1/1 + - host: 192.0.2.2 + informs: True + version: '3' + auth: NMS + users: + auth: + - user: snmp_user_1 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + - user: snmp_user_2 + group: network-operator + authentication: + algorithm: md5 + password: '0x5632724fb8ac3699296af26281e1d0f1' + localized_key: True + priv: + privacy_password: '0x5632724fb8ac3699296af26281e1d0f1' + aes_128: True + use_acls: + - user: snmp_user_1 + ipv4: acl1 + ipv6: acl2 + - user: snmp_user_2 + ipv4: acl3 + ipv6: acl4 + state: rendered + + +# Task Output (redacted) +# ----------------------- +# rendered: +# - snmp-server contact nxosswitchadmin@localhost +# - snmp-server location serverroom-1 +# - snmp-server aaa-user cache-timeout 36000 +# - snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# - snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# - snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# - snmp-server host 192.0.2.1 traps version 1 public +# - snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# - snmp-server host 192.0.2.2 informs version 3 auth NMS +# - snmp-server community private group network-admin +# - snmp-server community public group network-operator +# - snmp-server enable traps aaa server-state-change +# - snmp-server enable traps system Clock-change-notification + +# Using parsed + +# parsed.cfg +# ------------ +# snmp-server contact nxosswitchadmin@localhost +# snmp-server location serverroom-1 +# snmp-server aaa-user cache-timeout 36000 +# snmp-server user snmp_user_1 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_2 network-operator auth md5 0x5632724fb8ac3699296af26281e1d0f1 priv aes-128 0x5632724fb8ac3699296af26281e1d0f1 localizedkey +# snmp-server user snmp_user_1 use-ipv4acl acl1 use-ipv6acl acl2 +# snmp-server user snmp_user_2 use-ipv4acl acl3 use-ipv6acl acl4 +# snmp-server host 192.0.2.1 traps version 1 public +# snmp-server host 192.0.2.1 source-interface Ethernet1/1 +# snmp-server host 192.0.2.2 informs version 3 auth NMS +# snmp-server community private group network-admin +# snmp-server community public group network-operator +# snmp-server enable traps aaa server-state-change +# snmp-server enable traps system Clock-change-notification + +- name: Parse externally provided snmp-server configuration + cisco.nxos.nxos_snmp_server: + running_config: "{{ lookup('file', './parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- +# parsed: +# aaa_user: +# cache_timeout: 36000 +# communities: +# - community: private +# group: network-admin +# - community: public +# group: network-operator +# contact: nxosswitchadmin@localhost +# location: serverroom-1 +# traps: +# aaa: +# server_state_change: True +# system: +# clock_change_notification: True +# hosts: +# - host: 192.0.2.1 +# traps: true +# version: "1" +# community: public +# +# - host: 192.0.2.1 +# source_interface: Ethernet1/1 +# +# - host: 192.0.2.2 +# informs: true +# version: "3" +# auth: NMS +# users: +# auth: +# - user: snmp_user_1 +# group: network-operator +# authentication: +# algorithm: md5 +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# localized_key: True +# +# - authentication: +# algorithm: md5 +# localized_key: true +# password: "0x5632724fb8ac3699296af26281e1d0f1" +# priv: +# aes_128: true +# privacy_password: "0x5632724fb8ac3699296af26281e1d0f1" +# group: network-operator +# user: snmp_user_2 +# +# use_acls: +# - user: snmp_user_1 +# ipv4: acl1 +# ipv6: acl2 +# - user: snmp_user_2 +# ipv4: acl3 +# ipv6: acl4 +# +""" + +RETURN = """ +before: + description: The configuration prior to the module execution. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: dict + sample: > + This output will always be in the same format as the + module argspec. +after: + description: The resulting configuration after module execution. + returned: when changed + type: dict + sample: > + This output will always be in the same format as the + module argspec. +commands: + description: The set of commands pushed to the remote device. + returned: when I(state) is C(merged), C(replaced), C(overridden), C(deleted) or C(purged) + type: list + sample: + - sample command 1 + - sample command 2 + - sample command 3 +rendered: + description: The provided configuration in the task rendered in device-native format (offline). + returned: when I(state) is C(rendered) + type: list + sample: + - sample command 1 + - sample command 2 + - sample command 3 +gathered: + description: Facts about the network resource gathered from the remote device as structured data. + returned: when I(state) is C(gathered) + type: list + sample: > + This output will always be in the same format as the + module argspec. +parsed: + description: The device native config provided in I(running_config) option parsed into structured data as per module argspec. + returned: when I(state) is C(parsed) + type: list + sample: > + This output will always be in the same format as the + module argspec. +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.snmp_server.snmp_server import ( + Snmp_serverArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.snmp_server.snmp_server import ( + Snmp_server, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule( + argument_spec=Snmp_serverArgs.argument_spec, + mutually_exclusive=[["config", "running_config"]], + required_if=[ + ["state", "merged", ["config"]], + ["state", "replaced", ["config"]], + ["state", "overridden", ["config"]], + ["state", "rendered", ["config"]], + ["state", "parsed", ["running_config"]], + ], + supports_check_mode=True, + ) + + result = Snmp_server(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_traps.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_traps.py new file mode 100644 index 00000000..fce1a8a2 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_traps.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_traps +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP traps. +description: +- Manages SNMP traps configurations. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- This module works at the group level for traps. If you need to only enable/disable + 1 specific trap within a group, use the M(cisco.nxos.nxos_command) module. +- Be aware that you can set a trap only for an enabled feature. +options: + group: + description: + - Case sensitive group. + required: true + choices: + - aaa + - bfd + - bgp + - bridge + - callhome + - cfs + - config + - eigrp + - entity + - feature-control + - generic + - hsrp + - license + - link + - lldp + - mmode + - ospf + - pim + - rf + - rmon + - snmp + - storm-control + - stpx + - switchfabric + - syslog + - sysmgr + - system + - upgrade + - vtp + - all + type: str + state: + description: + - Manage the state of the resource. + required: false + default: enabled + choices: + - enabled + - disabled + type: str +""" + +EXAMPLES = """ +# ensure lldp trap configured +- cisco.nxos.nxos_snmp_traps: + group: lldp + state: enabled + +# ensure lldp trap is not configured +- cisco.nxos.nxos_snmp_traps: + group: lldp + state: disabled +""" + +RETURN = """ +commands: + description: command sent to the device + returned: always + type: list + sample: "snmp-server enable traps lldp ;" +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) + + +def get_platform_id(module): + info = get_capabilities(module).get("device_info", {}) + return info.get("network_os_platform", "") + + +def execute_show_command(command, module): + command = {"command": command, "output": "text"} + + return run_commands(module, command) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_traps(group, module): + body = execute_show_command("show run snmp all", module)[0].split("\n") + + resource = {} + feature_list = [ + "aaa", + "bfd", + "bgp", + "bridge", + "callhome", + "cfs", + "config", + "eigrp", + "entity", + "feature-control", + "generic", + "hsrp", + "license", + "link", + "lldp", + "mmode", + "ospf", + "pim", + "rf", + "rmon", + "snmp", + "storm-control", + "stpx", + "switchfabric", + "syslog", + "sysmgr", + "system", + "upgrade", + "vtp", + ] + + if "all" in group and "N3K-C35" in get_platform_id(module): + module.warn("Platform does not support bfd traps; bfd ignored for 'group: all' request") + feature_list.remove("bfd") + + for each in feature_list: + for line in body: + if each == "ospf": + # ospf behaves differently when routers are present + if "snmp-server enable traps ospf" == line: + resource[each] = True + break + else: + if "enable traps {0}".format(each) in line: + if "no " in line: + resource[each] = False + break + else: + resource[each] = True + + for each in feature_list: + if resource.get(each) is None: + # on some platforms, the 'no' cmd does not + # show up and so check if the feature is enabled + body = execute_show_command("show run | inc feature", module)[0] + if "feature {0}".format(each) in body: + resource[each] = False + + find = resource.get(group, None) + + if group == "all".lower(): + return resource + elif find is not None: + trap_resource = {group: find} + return trap_resource + else: + # if 'find' is None, it means that 'group' is a + # currently disabled feature. + return {} + + +def get_trap_commands(group, state, existing, module): + commands = [] + enabled = False + disabled = False + + if group == "all": + if state == "disabled": + for feature in existing: + if existing[feature]: + trap_command = "no snmp-server enable traps {0}".format(feature) + commands.append(trap_command) + + elif state == "enabled": + for feature in existing: + if existing[feature] is False: + trap_command = "snmp-server enable traps {0}".format(feature) + commands.append(trap_command) + + else: + if group in existing: + if existing[group]: + enabled = True + else: + disabled = True + + if state == "disabled" and enabled: + commands.append(["no snmp-server enable traps {0}".format(group)]) + elif state == "enabled" and disabled: + commands.append(["snmp-server enable traps {0}".format(group)]) + else: + module.fail_json(msg="{0} is not a currently " "enabled feature.".format(group)) + + return commands + + +def main(): + argument_spec = dict( + state=dict(choices=["enabled", "disabled"], default="enabled"), + group=dict( + choices=[ + "aaa", + "bfd", + "bgp", + "bridge", + "callhome", + "cfs", + "config", + "eigrp", + "entity", + "feature-control", + "generic", + "hsrp", + "license", + "link", + "lldp", + "mmode", + "ospf", + "pim", + "rf", + "rmon", + "snmp", + "storm-control", + "stpx", + "switchfabric", + "syslog", + "sysmgr", + "system", + "upgrade", + "vtp", + "all", + ], + required=True, + ), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + group = module.params["group"].lower() + state = module.params["state"] + + existing = get_snmp_traps(group, module) + + commands = get_trap_commands(group, state, existing, module) + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_user.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_user.py new file mode 100644 index 00000000..a3b0b5fa --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_snmp_user.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_snmp_user +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: (deprecated, removed after 2024-01-01) Manages SNMP users for monitoring. +description: +- Manages SNMP user configuration. +version_added: 1.0.0 +deprecated: + alternative: nxos_snmp_server + why: Updated modules released with more functionality + removed_at_date: '2024-01-01' +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Limited Support for Cisco MDS +- Authentication parameters not idempotent. +options: + user: + description: + - Name of the user. + required: true + type: str + group: + description: + - Group to which the user will belong to. If state = present, and the user is + existing, the group is added to the user. If the user is not existing, user + entry is created with this group argument. If state = absent, only the group + is removed from the user entry. However, to maintain backward compatibility, + if the existing user belongs to only one group, and if group argument is same + as the existing user's group, then the user entry also is deleted. + type: str + authentication: + description: + - Authentication parameters for the user. + choices: + - md5 + - sha + type: str + pwd: + description: + - Authentication password when using md5 or sha. This is not idempotent + type: str + privacy: + description: + - Privacy password for the user. This is not idempotent + type: str + encrypt: + description: + - Enables AES-128 bit encryption when using privacy password. + type: bool + state: + description: + - Manage the state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_snmp_user: + user: ntc + group: network-operator + authentication: md5 + pwd: test_password +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server user ntc network-operator auth md5 test_password"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def execute_show_command(command, module, text=False): + command = {"command": command, "output": "json"} + if text: + command["output"] = "text" + + return run_commands(module, command) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_snmp_groups(module): + data = execute_show_command("show snmp group", module)[0] + group_list = [] + + try: + group_table = data["TABLE_role"]["ROW_role"] + for group in group_table: + group_list.append(group["role_name"]) + except (KeyError, AttributeError): + return group_list + + return group_list + + +def get_snmp_user(user, module): + command = "show snmp user {0}".format(user) + body = execute_show_command(command, module, text=True) + body_text = body[0] + + if "No such entry" not in body[0]: + body = execute_show_command(command, module) + + resource = {} + try: + # The TABLE and ROW keys differ between NXOS platforms. + if body[0].get("TABLE_snmp_user"): + tablekey = "TABLE_snmp_user" + rowkey = "ROW_snmp_user" + tablegrpkey = "TABLE_snmp_group_names" + rowgrpkey = "ROW_snmp_group_names" + authkey = "auth_protocol" + privkey = "priv_protocol" + grpkey = "group_names" + elif body[0].get("TABLE_snmp_users"): + tablekey = "TABLE_snmp_users" + rowkey = "ROW_snmp_users" + tablegrpkey = "TABLE_groups" + rowgrpkey = "ROW_groups" + authkey = "auth" + privkey = "priv" + grpkey = "group" + + rt = body[0][tablekey][rowkey] + # on some older platforms, all groups except the 1st one + # are in list elements by themselves and they are + # indexed by 'user'. This is due to a platform bug. + # Get first element if rt is a list due to the bug + # or if there is no bug, parse rt directly + if isinstance(rt, list): + resource_table = rt[0] + else: + resource_table = rt + + resource["user"] = user + resource["authentication"] = str(resource_table[authkey]).strip() + encrypt = str(resource_table[privkey]).strip() + if encrypt.startswith("aes"): + resource["encrypt"] = "aes-128" + else: + resource["encrypt"] = "none" + + groups = [] + if tablegrpkey in resource_table: + group_table = resource_table[tablegrpkey][rowgrpkey] + try: + for group in group_table: + groups.append(str(group[grpkey]).strip()) + except TypeError: + groups.append(str(group_table[grpkey]).strip()) + + # Now for the platform bug case, get the groups + if isinstance(rt, list): + # remove 1st element from the list as this is parsed already + rt.pop(0) + # iterate through other elements indexed by + # 'user' and add it to groups. + for each in rt: + groups.append(each["user"].strip()) + + # Some 'F' platforms use 'group' key instead + elif "group" in resource_table: + # single group is a string, multiple groups in a list + groups = resource_table["group"] + if isinstance(groups, str): + groups = [groups] + + resource["group"] = groups + + except (KeyError, AttributeError, IndexError, TypeError): + if not resource and body_text and "No such entry" not in body_text: + # 6K and other platforms may not return structured output; + # attempt to get state from text output + resource = get_non_structured_snmp_user(body_text) + + return resource + + +def get_non_structured_snmp_user(body_text): + # This method is a workaround for platforms that don't support structured + # output for 'show snmp user <foo>'. This workaround may not work on all + # platforms. Sample non-struct output: + # + # User Auth Priv(enforce) Groups acl_filter + # ____ ____ _____________ ______ __________ + # sample1 no no network-admin ipv4:my_acl + # network-operator + # priv-11 + # -OR- + # sample2 md5 des(no) priv-15 + # -OR- + # sample3 md5 aes-128(no) network-admin + resource = {} + output = body_text.rsplit("__________")[-1] + pat = re.compile( + r"^(?P<user>\S+)\s+" + r"(?P<auth>\S+)\s+" + r"(?P<priv>[\w\d-]+)(?P<enforce>\([\w\d-]+\))*\s+" + r"(?P<group>\S+)", + re.M, + ) + m = re.search(pat, output) + if not m: + return resource + resource["user"] = m.group("user") + resource["auth"] = m.group("auth") + resource["encrypt"] = "aes-128" if "aes" in str(m.group("priv")) else "none" + + resource["group"] = [m.group("group")] + more_groups = re.findall(r"^\s+([\w\d-]+)\s*$", output, re.M) + if more_groups: + resource["group"] += more_groups + + return resource + + +def remove_snmp_user(user, group=None): + if group: + return ["no snmp-server user {0} {1}".format(user, group)] + else: + return ["no snmp-server user {0}".format(user)] + + +def config_snmp_user(proposed, user, reset): + if reset: + commands = remove_snmp_user(user) + else: + commands = [] + + if proposed.get("group"): + cmd = "snmp-server user {0} {group}".format(user, **proposed) + else: + cmd = "snmp-server user {0}".format(user) + + auth = proposed.get("authentication", None) + pwd = proposed.get("pwd", None) + + if auth and pwd: + cmd += " auth {authentication} {pwd}".format(**proposed) + + encrypt = proposed.get("encrypt", None) + privacy = proposed.get("privacy", None) + + if encrypt and privacy: + cmd += " priv {encrypt} {privacy}".format(**proposed) + elif privacy: + cmd += " priv {privacy}".format(**proposed) + + if cmd: + commands.append(cmd) + + return commands + + +def main(): + argument_spec = dict( + user=dict(required=True, type="str"), + group=dict(type="str"), + pwd=dict(type="str", no_log=True), + privacy=dict(type="str"), + authentication=dict(choices=["md5", "sha"]), + encrypt=dict(type="bool"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + required_together=[["authentication", "pwd"], ["encrypt", "privacy"]], + supports_check_mode=True, + ) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + user = module.params["user"] + group = module.params["group"] + pwd = module.params["pwd"] + privacy = module.params["privacy"] + encrypt = module.params["encrypt"] + authentication = module.params["authentication"] + state = module.params["state"] + + if privacy and encrypt: + if not pwd and authentication: + module.fail_json( + msg="pwd and authentication must be provided " "when using privacy and encrypt", + ) + + if group and group not in get_snmp_groups(module): + module.fail_json(msg="group not configured yet on switch.") + + existing = get_snmp_user(user, module) + + if state == "present" and existing: + if group: + if group not in existing["group"]: + existing["group"] = None + else: + existing["group"] = group + else: + existing["group"] = None + + commands = [] + + if state == "absent" and existing: + if group: + if group in existing["group"]: + if len(existing["group"]) == 1: + commands.append(remove_snmp_user(user)) + else: + commands.append(remove_snmp_user(user, group)) + else: + commands.append(remove_snmp_user(user)) + + elif state == "present": + reset = False + + args = dict( + user=user, + pwd=pwd, + group=group, + privacy=privacy, + encrypt=encrypt, + authentication=authentication, + ) + proposed = dict((k, v) for k, v in args.items() if v is not None) + + if not existing: + if encrypt: + proposed["encrypt"] = "aes-128" + commands.append(config_snmp_user(proposed, user, reset)) + + elif existing: + if encrypt and not existing["encrypt"].startswith("aes"): + reset = True + proposed["encrypt"] = "aes-128" + + delta = dict(set(proposed.items()).difference(existing.items())) + + if delta.get("pwd"): + delta["authentication"] = authentication + + if delta and encrypt: + delta["encrypt"] = "aes-128" + + if delta: + command = config_snmp_user(delta, user, reset) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + + if "configure" in cmds: + cmds.pop(0) + results["commands"] = cmds + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_static_routes.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_static_routes.py new file mode 100644 index 00000000..15b8e690 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_static_routes.py @@ -0,0 +1,477 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# +""" +The module file for nxos_static_routes +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_static_routes +short_description: Static routes resource module +description: This module configures and manages the attributes of static routes on + Cisco NX-OS platforms. +version_added: 1.0.0 +author: Adharsh Srivats Rangarajan (@adharshsrivatsr) +notes: +- Tested against NX-OS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- When a route is configured for a non-existent VRF, the VRF is created and the route + is added to it. +- When deleting routes for a VRF, all routes inside the VRF are deleted, but the VRF + is not deleted. +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the following commands in order B(show running-config | include + '^ip(v6)* route') and B(show running-config | section '^vrf context'). + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: + - A list of configurations for static routes + type: list + elements: dict + suboptions: + vrf: + description: + - The VRF to which the static route(s) belong + type: str + address_families: + description: A dictionary specifying the address family to which the static + route(s) belong. + type: list + elements: dict + suboptions: + afi: + description: + - Specifies the top level address family indicator. + type: str + choices: [ipv4, ipv6] + required: true + routes: + description: A dictionary that specifies the static route configurations + elements: dict + type: list + suboptions: + dest: + description: + - Destination prefix of static route + - The address format is <ipv4/v6 address>/<mask> + - The mask is number in range 0-32 for IPv4 and in range 0-128 for + IPv6 + type: str + required: true + next_hops: + description: + - Details of route to be taken + type: list + elements: dict + suboptions: + forward_router_address: + description: + - IP address of the next hop router + type: str + # required: True + interface: + description: + - Outgoing interface to take. For anything except 'Null0', then + next hop IP address should also be configured. + type: str + admin_distance: + description: + - Preference or administrative distance of route (range 1-255) + type: int + route_name: + description: + - Name of the static route + type: str + tag: + description: + - Route tag value (numeric) + type: int + track: + description: + - Track value (range 1 - 512). Track must already be configured + on the device before adding the route. + type: int + dest_vrf: + description: + - VRF of the destination + type: str + state: + description: + - The state the configuration should be left in + type: str + choices: + - deleted + - merged + - overridden + - replaced + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using deleted: + +# Before state: +# ------------- +# +# ip route 192.0.2.32/28 192.0.2.12 name new_route +# ip route 192.0.2.26/24 192.0.2.13 tag 12 + +- name: Delete all routes + cisco.nxos.nxos_static_routes: + state: deleted + +# After state: +# ------------ +# + + +# Before state: +# ------------ +# +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ip route 192.0.2.64/28 192.0.2.22 tag 4 +# ip route 192.0.2.64/28 192.0.2.23 name merged_route 1 +# ipv6 route 2200:10::/36 2048:ae12::1 vrf dest 5 + +- name: Delete routes based on VRF + cisco.nxos.nxos_static_routes: + config: + - vrf: trial_vrf + state: deleted + +# After state: +# ----------- +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf + + +# Before state: +# ------------ +# +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ip route 192.0.2.64/28 192.0.2.22 tag 4 +# ip route 192.0.2.64/28 192.0.2.23 name merged_route 1 +# ipv6 route 2200:10::/36 2048:ae12::1 vrf dest 5 + +- name: Delete routes based on AFI in a VRF + cisco.nxos.nxos_static_routes: + config: + - vrf: trial_vrf + address_families: + - afi: ipv4 + state: deleted + +# After state: +# ----------- +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ipv6 route 2200:10::/36 2048:ae12::1 vrf dest 5 + + +# Before state: +# ----------- +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# vrf context trial_vrf +# ipv6 route 2200:10::/36 2048:ae12::1 vrf dest 5 + + +# Using merged + +# Before state: +# ------------- +# + +- name: Merge new static route configuration + cisco.nxos.nxos_static_routes: + config: + - vrf: trial_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.64/24 + next_hops: + - forward_router_address: 192.0.2.22 + tag: 4 + admin_distance: 2 + + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/24 + next_hops: + - forward_router_address: 192.0.2.24 + route_name: new_route + - afi: ipv6 + routes: + - dest: 2001:db8::/64 + next_hops: + - interface: eth1/3 + forward_router_address: 2001:db8::12 + state: merged + +# After state: +# ------------ +# +# ip route 192.0.2.16/24 192.0.2.24 name new_route +# ipv6 route 2001:db8::/64 Ethernet1/3 2001:db8::12 +# vrf context trial_vrf +# ip route 192.0.2.0/24 192.0.2.22 tag 4 2 + + +# Using overridden: + +# Before state: +# ------------- +# +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ip route 192.0.2.64/28 192.0.2.22 tag 4 +# ip route 192.0.2.64/28 192.0.2.23 name merged_route 1 + +- name: Overriden existing static route configuration with new configuration + cisco.nxos.nxos_static_routes: + config: + - vrf: trial_vrf + address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.23 + route_name: overridden_route1 + admin_distance: 3 + + - forward_router_address: 192.0.2.45 + route_name: overridden_route2 + dest_vrf: destinationVRF + interface: Ethernet1/2 + state: overridden + +# After state: +# ------------ +# +# ip route 192.0.2.16/28 192.0.2.23 name replaced_route1 3 +# ip route 192.0.2.16/28 Ethernet1/2 192.0.2.45 vrf destinationVRF name replaced_route2 + + +# Using replaced: + +# Before state: +# ------------ +# ip route 192.0.2.16/28 192.0.2.24 name new_route +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ip route 192.0.2.64/28 192.0.2.22 tag 4 +# ip route 192.0.2.64/28 192.0.2.23 name merged_route 1 + +- name: Replaced the existing static configuration of a prefix with new configuration + cisco.nxos.nxos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.16/28 + next_hops: + - forward_router_address: 192.0.2.23 + route_name: replaced_route1 + admin_distance: 3 + + - forward_router_address: 192.0.2.45 + route_name: replaced_route2 + dest_vrf: destinationVRF + interface: Ethernet1/2 + state: replaced + +# After state: +# ----------- +# ip route 192.0.2.16/28 192.0.2.23 name replaced_route1 3 +# ip route 192.0.2.16/28 Ethernet1/2 192.0.2.45 vrf destinationVRF name replaced_route2 +# ip route 192.0.2.80/28 192.0.2.26 tag 12 +# vrf context trial_vrf +# ip route 192.0.2.64/28 192.0.2.22 tag 4 +# ip route 192.0.2.64/28 192.0.2.23 name merged_route 1 + + +# Using gathered: + +# Before state: +# ------------- +# ipv6 route 2001:db8:12::/32 2001:db8::12 +# vrf context Test +# ip route 192.0.2.48/28 192.0.2.13 +# ip route 192.0.2.48/28 192.0.2.14 5 + +- name: Gather the exisitng condiguration + cisco.nxos.nxos_static_routes: + state: gathered + +# returns: +# gathered: +# - vrf: Test +# address_families: +# - afi: ipv4 +# routes: +# - dest: 192.0.2.48/28 +# next_hops: +# - forward_router_address: 192.0.2.13 +# +# - forward_router_address: 192.0.2.14 +# admin_distance: 5 +# +# - address_families: +# - afi: ipv6 +# routes: +# - dest: 2001:db8:12::/32 +# next_hops: +# - forward_router_address: 2001:db8::12 + + +# Using rendered: + +- name: Render required configuration to be pushed to the device + cisco.nxos.nxos_static_routes: + config: + - address_families: + - afi: ipv4 + routes: + - dest: 192.0.2.48/28 + next_hops: + - forward_router_address: 192.0.2.13 + + - afi: ipv6 + routes: + - dest: 2001:db8::/64 + next_hops: + - interface: eth1/3 + forward_router_address: 2001:db8::12 + state: rendered + +# returns +# rendered: +# vrf context default +# ip route 192.0.2.48/28 192.0.2.13 +# ipv6 route 2001:db8::/64 Ethernet1/3 2001:db8::12 + + +# Using parsed + +- name: Parse the config to structured data + cisco.nxos.nxos_static_routes: + running_config: | + ipv6 route 2002:db8:12::/32 2002:db8:12::1 + vrf context Test + ip route 192.0.2.48/28 192.0.2.13 + ip route 192.0.2.48/28 192.0.2.14 5 + +# returns: +# parsed: +# - vrf: Test +# address_families: +# - afi: ipv4 +# routes: +# - dest: 192.0.2.48/28 +# next_hops: +# - forward_router_address: 192.0.2.13 +# +# - forward_router_address: 192.0.2.14 +# admin_distance: 5 +# +# - address_families: +# - afi: ipv6 +# routes: +# - dest: 2002:db8:12::/32 +# next_hops: +# - forward_router_address: 2002:db8:12::1 + + +""" +RETURN = """ +before: + description: The configuration prior to the model invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The resulting configuration model invocation. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['ip route 192.0.2.48/28 192.0.2.12 Ethernet1/2 name sample_route', + 'ipv6 route 2001:db8:3000::/36 2001:db8:200:2::2', 'vrf context test','ip route 192.0.2.48/28 192.0.2.121'] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.static_routes.static_routes import ( + Static_routesArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.static_routes.static_routes import ( + Static_routes, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=Static_routesArgs.argument_spec, supports_check_mode=True) + + result = Static_routes(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_system.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_system.py new file mode 100644 index 00000000..df4bbde0 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_system.py @@ -0,0 +1,399 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_system +extends_documentation_fragment: +- cisco.nxos.nxos +author: Peter Sprygada (@privateip) +short_description: Manage the system attributes on Cisco NXOS devices +notes: +- Unsupported for Cisco MDS +description: +- This module provides declarative management of node system attributes on Cisco NXOS + devices. It provides an option to configure host system parameters or remove those + parameters from the device active configuration. +version_added: 1.0.0 +options: + hostname: + description: + - Configure the device hostname parameter. This option takes an ASCII string value + or keyword 'default' + type: str + domain_name: + description: + - Configures the default domain name suffix to be used when referencing this node + by its FQDN. This argument accepts either a list of domain names or a list + of dicts that configure the domain name and VRF name or keyword 'default'. See + examples. + type: list + elements: raw + domain_lookup: + description: + - Enables or disables the DNS lookup feature in Cisco NXOS. This argument accepts + boolean values. When enabled, the system will try to resolve hostnames using + DNS and when disabled, hostnames will not be resolved. + type: bool + domain_search: + description: + - Configures a list of domain name suffixes to search when performing DNS name + resolution. This argument accepts either a list of domain names or a list of + dicts that configure the domain name and VRF name or keyword 'default'. See + examples. + type: list + elements: raw + name_servers: + description: + - List of DNS name servers by IP address to use to perform name resolution lookups. This + argument accepts either a list of DNS servers or a list of hashes that configure + the name server and VRF name or keyword 'default'. See examples. + type: list + elements: raw + system_mtu: + description: + - Specifies the mtu, must be an integer or keyword 'default'. + type: str + state: + description: + - State of the configuration values in the device's current active configuration. When + set to I(present), the values should be configured in the device active configuration + and when set to I(absent) the values should not be in the device active configuration + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: configure hostname and domain-name + cisco.nxos.nxos_system: + hostname: nxos01 + domain_name: test.example.com + +- name: remove configuration + cisco.nxos.nxos_system: + state: absent + +- name: configure name servers + cisco.nxos.nxos_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 + +- name: configure name servers with VRF support + cisco.nxos.nxos_system: + name_servers: + - {server: 8.8.8.8, vrf: mgmt} + - {server: 8.8.4.4, vrf: mgmt} +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - hostname nxos01 + - ip domain-name test.example.com +""" +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, +) +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + ComplexList, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +_CONFIGURED_VRFS = None + + +def has_vrf(module, vrf): + global _CONFIGURED_VRFS + if _CONFIGURED_VRFS is not None: + return vrf in _CONFIGURED_VRFS + config = get_config(module) + _CONFIGURED_VRFS = re.findall(r"vrf context (\S+)", config) + return vrf in _CONFIGURED_VRFS + + +def map_obj_to_commands(want, have, module): + commands = list() + state = module.params["state"] + + def needs_update(x): + return want.get(x) and (want.get(x) != have.get(x)) + + def difference(x, y, z): + return [item for item in x[z] if item not in y[z]] + + def remove(cmd, commands, vrf=None): + if vrf: + commands.append("vrf context %s" % vrf) + commands.append(cmd) + if vrf: + commands.append("exit") + + def add(cmd, commands, vrf=None): + if vrf: + if not has_vrf(module, vrf): + module.fail_json(msg="invalid vrf name %s" % vrf) + return remove(cmd, commands, vrf) + + if state == "absent": + if have["hostname"]: + commands.append("no hostname") + + for item in have["domain_name"]: + cmd = "no ip domain-name %s" % item["name"] + remove(cmd, commands, item["vrf"]) + + for item in have["domain_search"]: + cmd = "no ip domain-list %s" % item["name"] + remove(cmd, commands, item["vrf"]) + + for item in have["name_servers"]: + cmd = "no ip name-server %s" % item["server"] + remove(cmd, commands, item["vrf"]) + + if have["system_mtu"]: + commands.append("no system jumbomtu") + + if state == "present": + if needs_update("hostname"): + if want["hostname"] == "default": + if have["hostname"]: + commands.append("no hostname") + else: + commands.append("hostname %s" % want["hostname"]) + + if want.get("domain_lookup") is not None: + if have.get("domain_lookup") != want.get("domain_lookup"): + cmd = "ip domain-lookup" + if want["domain_lookup"] is False: + cmd = "no %s" % cmd + commands.append(cmd) + + if want["domain_name"]: + if want.get("domain_name")[0]["name"] == "default": + if have["domain_name"]: + for item in have["domain_name"]: + cmd = "no ip domain-name %s" % item["name"] + remove(cmd, commands, item["vrf"]) + else: + for item in difference(have, want, "domain_name"): + cmd = "no ip domain-name %s" % item["name"] + remove(cmd, commands, item["vrf"]) + for item in difference(want, have, "domain_name"): + cmd = "ip domain-name %s" % item["name"] + add(cmd, commands, item["vrf"]) + + if want["domain_search"]: + if want.get("domain_search")[0]["name"] == "default": + if have["domain_search"]: + for item in have["domain_search"]: + cmd = "no ip domain-list %s" % item["name"] + remove(cmd, commands, item["vrf"]) + else: + for item in difference(have, want, "domain_search"): + cmd = "no ip domain-list %s" % item["name"] + remove(cmd, commands, item["vrf"]) + for item in difference(want, have, "domain_search"): + cmd = "ip domain-list %s" % item["name"] + add(cmd, commands, item["vrf"]) + + if want["name_servers"]: + if want.get("name_servers")[0]["server"] == "default": + if have["name_servers"]: + for item in have["name_servers"]: + cmd = "no ip name-server %s" % item["server"] + remove(cmd, commands, item["vrf"]) + else: + for item in difference(have, want, "name_servers"): + cmd = "no ip name-server %s" % item["server"] + remove(cmd, commands, item["vrf"]) + for item in difference(want, have, "name_servers"): + cmd = "ip name-server %s" % item["server"] + add(cmd, commands, item["vrf"]) + + if needs_update("system_mtu"): + if want["system_mtu"] == "default": + if have["system_mtu"]: + commands.append("no system jumbomtu") + else: + commands.append("system jumbomtu %s" % want["system_mtu"]) + + return commands + + +def parse_hostname(config): + match = re.search(r"^hostname (\S+)", config, re.M) + if match: + return match.group(1) + + +def parse_domain_name(config, vrf_config): + objects = list() + match = re.search(r"^ip domain-name (\S+)", config, re.M) + if match: + objects.append({"name": match.group(1), "vrf": None}) + + for vrf, cfg in iteritems(vrf_config): + match = re.search(r"ip domain-name (\S+)", cfg, re.M) + if match: + objects.append({"name": match.group(1), "vrf": vrf}) + + return objects + + +def parse_domain_search(config, vrf_config): + objects = list() + + for item in re.findall(r"^ip domain-list (\S+)", config, re.M): + objects.append({"name": item, "vrf": None}) + + for vrf, cfg in iteritems(vrf_config): + for item in re.findall(r"ip domain-list (\S+)", cfg, re.M): + objects.append({"name": item, "vrf": vrf}) + + return objects + + +def parse_name_servers(config, vrf_config, vrfs): + objects = list() + + match = re.search("^ip name-server (.+)$", config, re.M) + if match and "use-vrf" not in match.group(1): + for addr in match.group(1).split(" "): + objects.append({"server": addr, "vrf": None}) + + for vrf, cfg in iteritems(vrf_config): + vrf_match = re.search("ip name-server (.+)", cfg, re.M) + if vrf_match: + for addr in vrf_match.group(1).split(" "): + objects.append({"server": addr, "vrf": vrf}) + + return objects + + +def parse_system_mtu(config): + match = re.search(r"^system jumbomtu (\d+)", config, re.M) + if match: + return match.group(1) + + +def map_config_to_obj(module): + config = get_config(module) + configobj = NetworkConfig(indent=2, contents=config) + + vrf_config = {} + + vrfs = re.findall(r"^vrf context (\S+)$", config, re.M) + for vrf in vrfs: + config_data = configobj.get_block_config(path=["vrf context %s" % vrf]) + vrf_config[vrf] = config_data + + return { + "hostname": parse_hostname(config), + "domain_lookup": "no ip domain-lookup" not in config, + "domain_name": parse_domain_name(config, vrf_config), + "domain_search": parse_domain_search(config, vrf_config), + "name_servers": parse_name_servers(config, vrf_config, vrfs), + "system_mtu": parse_system_mtu(config), + } + + +def map_params_to_obj(module): + obj = { + "hostname": module.params["hostname"], + "domain_lookup": module.params["domain_lookup"], + "system_mtu": module.params["system_mtu"], + } + + domain_name = ComplexList(dict(name=dict(key=True), vrf=dict()), module) + + domain_search = ComplexList(dict(name=dict(key=True), vrf=dict()), module) + + name_servers = ComplexList(dict(server=dict(key=True), vrf=dict()), module) + + for arg, cast in [ + ("domain_name", domain_name), + ("domain_search", domain_search), + ("name_servers", name_servers), + ]: + if module.params[arg] is not None: + obj[arg] = cast(module.params[arg]) + else: + obj[arg] = None + + return obj + + +def main(): + """main entry point for module execution""" + argument_spec = dict( + hostname=dict(), + domain_lookup=dict(type="bool"), + # { name: <str>, vrf: <str> } + domain_name=dict(type="list", elements="raw"), + # {name: <str>, vrf: <str> } + domain_search=dict(type="list", elements="raw"), + # { server: <str>; vrf: <str> } + name_servers=dict(type="list", elements="raw"), + system_mtu=dict(type="str"), + state=dict(default="present", choices=["present", "absent"]), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(want, have, module) + result["commands"] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_telemetry.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_telemetry.py new file mode 100644 index 00000000..7498ff88 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_telemetry.py @@ -0,0 +1,339 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Cisco and/or its affiliates. +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_telemetry +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_telemetry +short_description: TELEMETRY resource module +description: Manages Telemetry Monitoring Service (TMS) configuration +version_added: 1.0.0 +author: Mike Wiebe (@mikewiebe) +notes: +- Supported on N9k Version 7.0(3)I7(5) and later. +- Unsupported for Cisco MDS +options: + config: + description: The provided configuration + type: dict + suboptions: + certificate: + type: dict + description: + - Certificate SSL/TLS and hostname values. + - Value must be a dict defining values for keys (key and hostname). + suboptions: + key: + description: + - Certificate key + type: str + hostname: + description: + - Certificate hostname + type: str + compression: + type: str + description: + - Destination profile compression method. + choices: + - gzip + source_interface: + type: str + description: + - Destination profile source interface. + - Valid value is a str representing the source interface name. + vrf: + type: str + description: + - Destination profile vrf. + - Valid value is a str representing the vrf name. + destination_groups: + type: list + description: + - List of telemetry destination groups. + elements: raw + suboptions: + id: + type: str + description: + - Destination group identifier. + - Value must be an integer or string representing the destination group identifier. + destination: + type: dict + description: + - Group destination ipv4, port, protocol and encoding values. + - Value must be a dict defining values for keys (ip, port, protocol, encoding). + suboptions: + ip: + type: str + description: + - Destination group IP address. + port: + type: int + description: + - Destination group port number. + protocol: + type: str + description: + - Destination group protocol. + choices: + - HTTP + - TCP + - UDP + - gRPC + encoding: + type: str + description: + - Destination group encoding. + choices: + - GPB + - JSON + sensor_groups: + type: list + description: + - List of telemetry sensor groups. + elements: raw + suboptions: + id: + type: str + description: + - Sensor group identifier. + - Value must be a integer or a string representing the sensor group identifier. + data_source: + type: str + description: + - Telemetry data source. + choices: + - NX-API + - DME + - YANG + path: + type: dict + description: + - Telemetry sensor path. + - Value must be a dict defining values for keys (name, depth, filter_condition, + query_condition). + - Mandatory Keys (name) + - Optional Keys (depth, filter_condition, query_condition) + suboptions: + name: + type: str + description: + - Sensor group path name. + depth: + type: str + description: + - Sensor group depth. + filter_condition: + type: str + description: + - Sensor group filter condition. + query_condition: + type: str + description: + - Sensor group query condition. + subscriptions: + type: list + description: + - List of telemetry subscriptions. + elements: raw + suboptions: + id: + type: str + description: + - Subscription identifier. + - Value must be an integer or string representing the subscription identifier. + destination_group: + type: str + description: + - Associated destination group. + sensor_group: + type: dict + description: + - Associated sensor group. + - Value must be a dict defining values for keys (id, sample_interval). + suboptions: + id: + type: str + description: + - Associated sensor group id. + sample_interval: + type: int + description: + - Associated sensor group id sample interval. + state: + description: + - Final configuration state + type: str + choices: + - merged + - replaced + - deleted + - gathered + default: merged + +""" +EXAMPLES = """ +# Using deleted +# This action will delete all telemetry configuration on the device + +- name: Delete Telemetry Configuration + cisco.nxos.nxos_telemetry: + state: deleted + + +# Using merged +# This action will merge telemetry configuration defined in the playbook with +# telemetry configuration that is already on the device. + +- name: Merge Telemetry Configuration + cisco.nxos.nxos_telemetry: + config: + certificate: + key: /bootflash/server.key + hostname: localhost + compression: gzip + source_interface: Ethernet1/1 + vrf: management + destination_groups: + - id: 2 + destination: + ip: 192.168.0.2 + port: 50001 + protocol: gRPC + encoding: GPB + - id: 55 + destination: + ip: 192.168.0.55 + port: 60001 + protocol: gRPC + encoding: GPB + sensor_groups: + - id: 1 + data_source: NX-API + path: + name: '"show lldp neighbors detail"' + depth: 0 + - id: 55 + data_source: DME + path: + name: sys/ch + depth: unbounded + filter_condition: ne(eqptFt.operSt,"ok") + subscriptions: + - id: 5 + destination_group: 55 + sensor_group: + id: 1 + sample_interval: 1000 + - id: 6 + destination_group: 2 + sensor_group: + id: 55 + sample_interval: 2000 + state: merged + + +# Using replaced +# This action will replace telemetry configuration on the device with the +# telemetry configuration defined in the playbook. + +- name: Override Telemetry Configuration + cisco.nxos.nxos_telemetry: + config: + certificate: + key: /bootflash/server.key + hostname: localhost + compression: gzip + source_interface: Ethernet1/1 + vrf: management + destination_groups: + - id: 2 + destination: + ip: 192.168.0.2 + port: 50001 + protocol: gRPC + encoding: GPB + subscriptions: + - id: 5 + destination_group: 55 + state: replaced + + +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: dict + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['command 1', 'command 2', 'command 3'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.telemetry.telemetry import ( + TelemetryArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.telemetry.telemetry import ( + Telemetry, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=TelemetryArgs.argument_spec, supports_check_mode=True) + + result = Telemetry(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_udld.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_udld.py new file mode 100644 index 00000000..83955e15 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_udld.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_udld +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages UDLD global configuration params. +description: +- Manages UDLD global configuration params. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Module will fail if the udld feature has not been previously enabled. +options: + aggressive: + description: + - Toggles aggressive mode. + choices: + - enabled + - disabled + type: str + msg_time: + description: + - Message time in seconds for UDLD packets or keyword 'default'. + type: str + reset: + description: + - Ability to reset all ports shut down by UDLD. 'state' parameter cannot be 'absent' + when this is present. + type: bool + state: + description: + - Manage the state of the resource. When set to 'absent', aggressive and msg_time + are set to their default values. + default: present + choices: + - present + - absent + type: str +""" +EXAMPLES = """ +# ensure udld aggressive mode is globally disabled and se global message interval is 20 +- cisco.nxos.nxos_udld: + aggressive: disabled + msg_time: 20 + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' + +# Ensure agg mode is globally enabled and msg time is 15 +- cisco.nxos.nxos_udld: + aggressive: enabled + msg_time: 15 + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"aggressive": "enabled", "msg_time": "40"} +existing: + description: + - k/v pairs of existing udld configuration + returned: always + type: dict + sample: {"aggressive": "disabled", "msg_time": "15"} +end_state: + description: k/v pairs of udld configuration after module execution + returned: always + type: dict + sample: {"aggressive": "enabled", "msg_time": "40"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["udld message-time 40", "udld aggressive"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +PARAM_TO_DEFAULT_KEYMAP = {"msg_time": "15"} + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = str(value) + else: + new_dict[new_key] = value + return new_dict + + +def get_commands_config_udld_global(delta, reset, existing): + commands = [] + for param, value in delta.items(): + if param == "aggressive": + command = "udld aggressive" if value == "enabled" else "no udld aggressive" + commands.append(command) + elif param == "msg_time": + if value == "default": + if existing.get("msg_time") != PARAM_TO_DEFAULT_KEYMAP.get("msg_time"): + commands.append("no udld message-time") + else: + commands.append("udld message-time " + value) + if reset: + command = "udld reset" + commands.append(command) + return commands + + +def get_commands_remove_udld_global(existing): + commands = [] + if existing.get("aggressive") == "enabled": + command = "no udld aggressive" + commands.append(command) + if existing.get("msg_time") != PARAM_TO_DEFAULT_KEYMAP.get("msg_time"): + command = "no udld message-time" + commands.append(command) + return commands + + +def get_udld_global(module): + command = "show udld global | json" + udld_table = run_commands(module, [command])[0] + + status = str(udld_table.get("udld-global-mode", None)) + if status == "enabled-aggressive": + aggressive = "enabled" + else: + aggressive = "disabled" + + interval = str(udld_table.get("message-interval", None)) + udld = dict(msg_time=interval, aggressive=aggressive) + + return udld + + +def main(): + argument_spec = dict( + aggressive=dict(required=False, choices=["enabled", "disabled"]), + msg_time=dict(required=False, type="str"), + reset=dict(required=False, type="bool"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + aggressive = module.params["aggressive"] + msg_time = module.params["msg_time"] + reset = module.params["reset"] + state = module.params["state"] + + if reset and state == "absent": + module.fail_json(msg="state must be present when using reset flag.") + + args = dict(aggressive=aggressive, msg_time=msg_time, reset=reset) + proposed = dict((k, v) for k, v in args.items() if v is not None) + + existing = get_udld_global(module) + end_state = existing + + delta = set(proposed.items()).difference(existing.items()) + changed = False + + commands = [] + if state == "present": + if delta: + command = get_commands_config_udld_global(dict(delta), reset, existing) + commands.append(command) + + elif state == "absent": + command = get_commands_remove_udld_global(existing) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_udld_global(module) + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["end_state"] = end_state + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_udld_interface.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_udld_interface.py new file mode 100644 index 00000000..b78ed5c7 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_udld_interface.py @@ -0,0 +1,301 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_udld_interface +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages UDLD interface configuration params. +description: +- Manages UDLD interface configuration params. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Feature UDLD must be enabled on the device to use this module. +options: + mode: + description: + - Manages UDLD mode for an interface. + required: true + choices: + - enabled + - disabled + - aggressive + type: str + interface: + description: + - FULL name of the interface, i.e. Ethernet1/1- + required: true + type: str + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: + - present + - absent + type: str +""" +EXAMPLES = """ +# ensure Ethernet1/1 is configured to be in aggressive mode +- cisco.nxos.nxos_udld_interface: + interface: Ethernet1/1 + mode: aggressive + state: present + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' + +# Remove the aggressive config only if it's currently in aggressive mode and then disable udld (switch default) +- cisco.nxos.nxos_udld_interface: + interface: Ethernet1/1 + mode: aggressive + state: absent + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' + +# ensure Ethernet1/1 has aggressive mode enabled +- cisco.nxos.nxos_udld_interface: + interface: Ethernet1/1 + mode: enabled + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"mode": "enabled"} +existing: + description: + - k/v pairs of existing configuration + returned: always + type: dict + sample: {"mode": "aggressive"} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"mode": "enabled"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface ethernet1/33", + "no udld aggressive ; no udld disable"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_udld_interface(module, interface): + command = "show run udld all | section " + interface.title() + "$" + interface_udld = {} + mode = None + mode_str = None + try: + body = run_commands(module, [{"command": command, "output": "text"}])[0] + if "aggressive" in body: + mode = "aggressive" + mode_str = "aggressive" + elif "no udld enable" in body: + mode = "disabled" + mode_str = "no udld enable" + elif "no udld disable" in body: + mode = "enabled" + mode_str = "no udld disable" + elif "udld disable" in body: + mode = "disabled" + mode_str = "udld disable" + elif "udld enable" in body: + mode = "enabled" + mode_str = "udld enable" + interface_udld["mode"] = mode + + except (KeyError, AttributeError, IndexError): + interface_udld = {} + + return interface_udld, mode_str + + +def get_commands_config_udld_interface1(delta, interface, module, existing): + commands = [] + mode = delta["mode"] + if mode == "aggressive": + commands.append("udld aggressive") + else: + commands.append("no udld aggressive") + commands.insert(0, "interface {0}".format(interface)) + + return commands + + +def get_commands_config_udld_interface2(delta, interface, module, existing): + commands = [] + existing, mode_str = get_udld_interface(module, interface) + mode = delta["mode"] + if mode == "enabled": + if mode_str == "no udld enable": + command = "udld enable" + else: + command = "no udld disable" + else: + if mode_str == "no udld disable": + command = "udld disable" + else: + command = "no udld enable" + if command: + commands.append(command) + commands.insert(0, "interface {0}".format(interface)) + + return commands + + +def get_commands_remove_udld_interface(delta, interface, module, existing): + commands = [] + existing, mode_str = get_udld_interface(module, interface) + + mode = delta["mode"] + if mode == "aggressive": + command = "no udld aggressive" + else: + if mode == "enabled": + if mode_str == "udld enable": + command = "no udld enable" + else: + command = "udld disable" + elif mode == "disabled": + if mode_str == "no udld disable": + command = "udld disable" + else: + command = "no udld enable" + if command: + commands.append(command) + commands.insert(0, "interface {0}".format(interface)) + + return commands + + +def main(): + argument_spec = dict( + mode=dict(choices=["enabled", "disabled", "aggressive"], required=True), + interface=dict(type="str", required=True), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + interface = module.params["interface"].lower() + mode = module.params["mode"] + state = module.params["state"] + + proposed = dict(mode=mode) + existing, mode_str = get_udld_interface(module, interface) + end_state = existing + + delta = dict(set(proposed.items()).difference(existing.items())) + + changed = False + commands = [] + cmds = [] + if state == "present": + if delta: + command = get_commands_config_udld_interface1(delta, interface, module, existing) + commands.append(command) + cmds = flatten_list(commands) + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + + if delta["mode"] == "enabled" or delta["mode"] == "disabled": + commands = [] + command = get_commands_config_udld_interface2(delta, interface, module, existing) + commands.append(command) + cmds = flatten_list(commands) + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + load_config(module, cmds) + + else: + common = set(proposed.items()).intersection(existing.items()) + if common: + command = get_commands_remove_udld_interface(dict(common), interface, module, existing) + cmds = flatten_list(commands) + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + + if not module.check_mode: + end_state, mode_str = get_udld_interface(module, interface) + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["end_state"] = end_state + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_user.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_user.py new file mode 100644 index 00000000..83c22a76 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_user.py @@ -0,0 +1,473 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_user +extends_documentation_fragment: +- cisco.nxos.nxos +author: Peter Sprygada (@privateip) +notes: +- Limited Support for Cisco MDS +short_description: Manage the collection of local users on Nexus devices +description: +- This module provides declarative management of the local usernames configured on + Cisco Nexus devices. It allows playbooks to manage either individual usernames + or the collection of usernames in the current running config. It also supports + purging usernames from the configuration that are not explicitly defined. +version_added: 1.0.0 +options: + aggregate: + description: + - The set of username objects to be configured on the remote Cisco Nexus device. The + list entries can either be the username or a hash of username and properties. This + argument is mutually exclusive with the C(name) argument. + aliases: + - users + - collection + type: list + elements: dict + suboptions: + name: + description: + - The username to be configured on the remote Cisco Nexus device. This argument + accepts a string value and is mutually exclusive with the C(aggregate) argument. + type: str + configured_password: + description: + - The password to be configured on the network device. The password needs to be + provided in cleartext and it will be encrypted on the device. + type: str + update_password: + description: + - Since passwords are encrypted in the device running config, this argument will + instruct the module when to change the password. When set to C(always), the + password will always be updated in the device and when set to C(on_create) the + password will be updated only if the username is created. + choices: + - on_create + - always + type: str + roles: + description: + - The C(role) argument configures the role for the username in the device running + configuration. The argument accepts a string value defining the role name. This + argument does not check if the role has been configured on the device. + aliases: + - role + type: list + elements: str + sshkey: + description: + - The C(sshkey) argument defines the SSH public key to configure for the username. This + argument accepts a valid SSH key value. + type: str + state: + description: + - The C(state) argument configures the state of the username definition as it + relates to the device operational configuration. When set to I(present), the + username(s) should be configured in the device active configuration and when + set to I(absent) the username(s) should not be in the device active configuration + choices: + - present + - absent + type: str + name: + description: + - The username to be configured on the remote Cisco Nexus device. This argument + accepts a string value and is mutually exclusive with the C(aggregate) argument. + type: str + configured_password: + description: + - The password to be configured on the network device. The password needs to be + provided in cleartext and it will be encrypted on the device. + type: str + update_password: + description: + - Since passwords are encrypted in the device running config, this argument will + instruct the module when to change the password. When set to C(always), the + password will always be updated in the device and when set to C(on_create) the + password will be updated only if the username is created. + default: always + choices: + - on_create + - always + type: str + roles: + description: + - The C(role) argument configures the role for the username in the device running + configuration. The argument accepts a string value defining the role name. This + argument does not check if the role has been configured on the device. + aliases: + - role + type: list + elements: str + sshkey: + description: + - The C(sshkey) argument defines the SSH public key to configure for the username. This + argument accepts a valid SSH key value. + type: str + purge: + description: + - The C(purge) argument instructs the module to consider the resource definition + absolute. It will remove any previously configured usernames on the device + with the exception of the `admin` user which cannot be deleted per nxos constraints. + type: bool + default: no + state: + description: + - The C(state) argument configures the state of the username definition as it + relates to the device operational configuration. When set to I(present), the + username(s) should be configured in the device active configuration and when + set to I(absent) the username(s) should not be in the device active configuration + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: create a new user + cisco.nxos.nxos_user: + name: ansible + sshkey: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}" + state: present + +- name: remove all users except admin + cisco.nxos.nxos_user: + purge: yes + +- name: set multiple users role + cisco.nxos.nxos_user: + aggregate: + - name: netop + - name: netend + role: network-operator + state: present +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - name ansible + - name ansible password password +""" +import re + +from copy import deepcopy +from functools import partial + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.six import iteritems +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, + to_list, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, + run_commands, +) + + +BUILTIN_ROLES = [ + "network-admin", + "network-operator", + "vdc-admin", + "vdc-operator", + "priv-15", + "priv-14", + "priv-13", + "priv-12", + "priv-11", + "priv-10", + "priv-9", + "priv-8", + "priv-7", + "priv-6", + "priv-5", + "priv-4", + "priv-3", + "priv-2", + "priv-1", + "priv-0", +] + + +def get_custom_roles(module): + return re.findall( + r"^role name (\S+)", + get_config(module, flags=["| include '^role name'"]), + re.M, + ) + + +def validate_roles(value, module): + valid_roles = BUILTIN_ROLES + get_custom_roles(module) + for item in value: + if item not in valid_roles: + module.fail_json(msg="invalid role specified") + + +def map_obj_to_commands(updates, module): + commands = list() + update_password = module.params["update_password"] + + for update in updates: + want, have = update + + def needs_update(x): + return want.get(x) and (want.get(x) != have.get(x)) + + def add(x): + return commands.append("username %s %s" % (want["name"], x)) + + def remove(x): + return commands.append("no username %s %s" % (want["name"], x)) + + def configure_roles(): + if want["roles"]: + if have: + for item in set(have["roles"]).difference(want["roles"]): + remove("role %s" % item) + + for item in set(want["roles"]).difference(have["roles"]): + add("role %s" % item) + else: + for item in want["roles"]: + add("role %s" % item) + + return True + return False + + if want["state"] == "absent": + commands.append("no username %s" % want["name"]) + continue + + roles_configured = False + if want["state"] == "present" and not have: + roles_configured = configure_roles() + if not roles_configured: + commands.append("username %s" % want["name"]) + + if needs_update("configured_password"): + if update_password == "always" or not have: + add("password %s" % want["configured_password"]) + + if needs_update("sshkey"): + add("sshkey %s" % want["sshkey"]) + + if not roles_configured: + configure_roles() + + return commands + + +def parse_password(data): + if not data.get("remote_login"): + return "<PASSWORD>" + + +def parse_roles(data): + configured_roles = None + if "TABLE_role" in data: + configured_roles = data.get("TABLE_role")["ROW_role"] + + roles = list() + if configured_roles: + for item in to_list(configured_roles): + roles.append(item["role"]) + return roles + + +def map_config_to_obj(module): + out = run_commands(module, [{"command": "show user-account", "output": "json"}]) + data = out[0] + + objects = list() + + for item in to_list(data["TABLE_template"]["ROW_template"]): + objects.append( + { + "name": item["usr_name"], + "configured_password": parse_password(item), + "sshkey": item.get("sshkey_info"), + "roles": parse_roles(item), + "state": "present", + }, + ) + return objects + + +def get_param_value(key, item, module): + # if key doesn't exist in the item, get it from module.params + if not item.get(key): + value = module.params[key] + + # if key does exist, do a type check on it to validate it + else: + value_type = module.argument_spec[key].get("type", "str") + type_checker = module._CHECK_ARGUMENT_TYPES_DISPATCHER[value_type] + type_checker(item[key]) + value = item[key] + + return value + + +def map_params_to_obj(module): + aggregate = module.params["aggregate"] + if not aggregate: + if not module.params["name"] and module.params["purge"]: + return list() + elif not module.params["name"]: + module.fail_json(msg="username is required") + else: + collection = [{"name": module.params["name"]}] + else: + collection = list() + for item in aggregate: + if not isinstance(item, dict): + collection.append({"name": item}) + elif "name" not in item: + module.fail_json(msg="name is required") + else: + collection.append(item) + + objects = list() + + for item in collection: + get_value = partial(get_param_value, item=item, module=module) + item.update( + { + "configured_password": get_value("configured_password"), + "sshkey": get_value("sshkey"), + "roles": get_value("roles"), + "state": get_value("state"), + }, + ) + + for key, value in iteritems(item): + if value: + # validate the param value (if validator func exists) + validator = globals().get("validate_%s" % key) + if all((value, validator)): + validator(value, module) + + objects.append(item) + + return objects + + +def update_objects(want, have): + updates = list() + for entry in want: + item = next((i for i in have if i["name"] == entry["name"]), None) + if all((item is None, entry["state"] == "present")): + updates.append((entry, {})) + elif item: + for key, value in iteritems(entry): + if value and value != item[key]: + updates.append((entry, item)) + return updates + + +def main(): + """main entry point for module execution""" + element_spec = dict( + name=dict(), + configured_password=dict(no_log=True), + update_password=dict(default="always", choices=["on_create", "always"]), + roles=dict(type="list", aliases=["role"], elements="str"), + sshkey=dict(no_log=False), + state=dict(default="present", choices=["present", "absent"]), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict( + type="list", + elements="dict", + options=aggregate_spec, + aliases=["collection", "users"], + ), + purge=dict(type="bool", default=False), + ) + + argument_spec.update(element_spec) + + mutually_exclusive = [("name", "aggregate")] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + result = {"changed": False, "warnings": []} + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(update_objects(want, have), module) + + if module.params["purge"]: + want_users = [x["name"] for x in want] + have_users = [x["name"] for x in have] + for item in set(have_users).difference(want_users): + if item != "admin": + item = item.replace("\\", "\\\\") + commands.append("no username %s" % item) + + result["commands"] = commands + + # the nxos cli prevents this by rule so capture it and display + # a nice failure message + if "no username admin" in commands: + module.fail_json(msg="cannot delete the `admin` account") + + if commands: + if not module.check_mode: + responses = load_config(module, commands) + for resp in responses: + if resp.lower().startswith("wrong password"): + module.fail_json(msg=resp) + else: + result["warnings"].extend( + [x[9:] for x in resp.splitlines() if x.startswith("WARNING: ")], + ) + + result["changed"] = True + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vlans.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vlans.py new file mode 100644 index 00000000..cac276b6 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vlans.py @@ -0,0 +1,439 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2019 Red Hat +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +############################################# +# WARNING # +############################################# +# +# This file is auto generated by the resource +# module builder playbook. +# +# Do not edit this file manually. +# +# Changes to this file will be over written +# by the resource module builder. +# +# Changes should be made in the model used to +# generate this file or in the resource module +# builder template. +# +############################################# + +""" +The module file for nxos_vlans +""" + +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vlans +short_description: VLANs resource module +description: This module creates and manages VLAN configurations on Cisco NX-OS. +version_added: 1.0.0 +author: Trishna Guha (@trishnaguha) +notes: +- Tested against NXOS 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +options: + running_config: + description: + - This option is used only with state I(parsed). + - The value of this option should be the output received from the NX-OS device + by executing the commands B(show vlans | json-pretty) and B(show running-config + | section ^vlan) in order and delimited by a line. + - The state I(parsed) reads the configuration from C(running_config) option and + transforms it into Ansible structured data as per the resource module's argspec + and the value is then returned in the I(parsed) key within the result. + type: str + config: + description: A dictionary of Vlan options + type: list + elements: dict + suboptions: + vlan_id: + description: + - Vlan ID. + type: int + required: true + name: + description: + - Name of VLAN. + type: str + state: + description: + - Manage operational state of the vlan. + type: str + choices: + - active + - suspend + enabled: + description: + - Manage administrative state of the vlan. + type: bool + mode: + description: + - Set vlan mode to classical ethernet or fabricpath. This is a valid option + for Nexus 5000, 6000 and 7000 series. + type: str + choices: + - ce + - fabricpath + mapped_vni: + description: + - The Virtual Network Identifier (VNI) ID that is mapped to the VLAN. + type: int + state: + description: + - The state of the configuration after module completion. + - The state I(overridden) would override the configuration of all the + VLANs on the device (including VLAN 1) with the provided configuration in + the task. Use caution with this state. + type: str + choices: + - merged + - replaced + - overridden + - deleted + - gathered + - rendered + - parsed + default: merged + +""" +EXAMPLES = """ +# Using merged + +# Before state: +# ------------- +# vlan 1 + +- name: Merge provided configuration with device configuration. + cisco.nxos.nxos_vlans: + config: + - vlan_id: 5 + name: test-vlan5 + - vlan_id: 10 + enabled: false + state: merged + +# After state: +# ------------ +# vlan 5 +# name test-vlan5 +# state active +# no shutdown +# vlan 10 +# state active +# shutdown + + +# Using replaced + +# Before state: +# ------------- +# vlan 1 +# vlan 5 +# name test-vlan5 +# vlan 10 +# shutdown + +- name: Replace device configuration of specified vlan with provided configuration. + cisco.nxos.nxos_vlans: + config: + - vlan_id: 5 + name: test-vlan + enabled: false + - vlan_id: 10 + enabled: false + state: replaced + +# After state: +# ------------ +# vlan 1 +# vlan 5 +# name test-vlan +# state active +# shutdown +# vlan 10 +# state active +# shutdown + + +# Using overridden + +# Before state: +# ------------- +# vlan 1 +# vlan 3 +# name testing +# vlan 5 +# name test-vlan5 +# shutdown +# vlan 10 +# shutdown + +- name: Override device configuration of all vlans with provided configuration. + cisco.nxos.nxos_vlans: + config: + - vlan_id: 5 + name: test-vlan + - vlan_id: 10 + state: active + state: overridden + +# After state: +# ------------ +# vlan 5 +# name test-vlan +# state active +# no shutdown +# vlan 10 +# state active +# no shutdown + + +# Using deleted + +# Before state: +# ------------- +# vlan 1 +# vlan 5 +# vlan 10 + +- name: Delete vlans. + cisco.nxos.nxos_vlans: + config: + - vlan_id: 5 + - vlan_id: 10 + state: deleted + +# After state: +# ------------ +# + +# Using rendered + +- name: Use rendered state to convert task input to device specific commands + cisco.nxos.nxos_vlans: + config: + - vlan_id: 5 + name: vlan5 + mapped_vni: 100 + + - vlan_id: 6 + name: vlan6 + state: suspend + state: rendered + +# Task Output (redacted) +# ----------------------- + +# rendered: +# - vlan 5 +# - name vlan5 +# - vn-segment 100 +# - vlan 6 +# - name vlan6 +# - state suspend + +# Using parsed + +# parsed.cfg +# ------------ +# { +# "TABLE_vlanbrief": { +# "ROW_vlanbrief": [ +# { +# "vlanshowbr-vlanid": "1", +# "vlanshowbr-vlanid-utf": "1", +# "vlanshowbr-vlanname": "default", +# "vlanshowbr-vlanstate": "active", +# "vlanshowbr-shutstate": "noshutdown" +# }, +# { +# "vlanshowbr-vlanid": "5", +# "vlanshowbr-vlanid-utf": "5", +# "vlanshowbr-vlanname": "vlan5", +# "vlanshowbr-vlanstate": "suspend", +# "vlanshowbr-shutstate": "noshutdown" +# }, +# { +# "vlanshowbr-vlanid": "6", +# "vlanshowbr-vlanid-utf": "6", +# "vlanshowbr-vlanname": "VLAN0006", +# "vlanshowbr-vlanstate": "active", +# "vlanshowbr-shutstate": "noshutdown" +# }, +# { +# "vlanshowbr-vlanid": "7", +# "vlanshowbr-vlanid-utf": "7", +# "vlanshowbr-vlanname": "vlan7", +# "vlanshowbr-vlanstate": "active", +# "vlanshowbr-shutstate": "noshutdown" +# } +# ] +# }, +# "TABLE_mtuinfo": { +# "ROW_mtuinfo": [ +# { +# "vlanshowinfo-vlanid": "1", +# "vlanshowinfo-media-type": "enet", +# "vlanshowinfo-vlanmode": "ce-vlan" +# }, +# { +# "vlanshowinfo-vlanid": "5", +# "vlanshowinfo-media-type": "enet", +# "vlanshowinfo-vlanmode": "ce-vlan" +# }, +# { +# "vlanshowinfo-vlanid": "6", +# "vlanshowinfo-media-type": "enet", +# "vlanshowinfo-vlanmode": "ce-vlan" +# }, +# { +# "vlanshowinfo-vlanid": "7", +# "vlanshowinfo-media-type": "enet", +# "vlanshowinfo-vlanmode": "ce-vlan" +# } +# ] +# } +# } +# +# vlan 1,5-7 +# vlan 5 +# state suspend +# name vlan5 +# vlan 7 +# name vlan7 +# vn-segment 100 + +- name: Use parsed state to convert externally supplied config to structured format + cisco.nxos.nxos_vlans: + running_config: "{{ lookup('file', 'parsed.cfg') }}" + state: parsed + +# Task output (redacted) +# ----------------------- + +# parsed: +# - vlan_id: 5 +# enabled: True +# mode: "ce" +# name: "vlan5" +# state: suspend +# +# - vlan_id: 6 +# enabled: True +# mode: "ce" +# state: active +# +# - vlan_id: 7 +# enabled: True +# mode: "ce" +# name: "vlan7" +# state: active +# mapped_vni: 100 + +# Using gathered + +# Existing device config state +# ------------------------------- +# nxos-9k# show vlan | json +# {"TABLE_vlanbrief": {"ROW_vlanbrief": [{"vlanshowbr-vlanid": "1", "vlanshowbr-vlanid-utf": "1", "vlanshowbr-vlanname": "default", "vlanshowbr-vlanstate +# ": "active", "vlanshowbr-shutstate": "noshutdown"}, {"vlanshowbr-vlanid": "5", "vlanshowbr-vlanid-utf": "5", "vlanshowbr-vlanname": "vlan5", "vlanshowb +# r-vlanstate": "suspend", "vlanshowbr-shutstate": "noshutdown"}, {"vlanshowbr-vlanid": "6", "vlanshowbr-vlanid-utf": "6", "vlanshowbr-vlanname": "VLAN00 +# 06", "vlanshowbr-vlanstate": "active", "vlanshowbr-shutstate": "noshutdown"}, {"vlanshowbr-vlanid": "7", "vlanshowbr-vlanid-utf": "7", "vlanshowbr-vlan +# name": "vlan7", "vlanshowbr-vlanstate": "active", "vlanshowbr-shutstate": "shutdown"}]}, "TABLE_mtuinfo": {"ROW_mtuinfo": [{"vlanshowinfo-vlanid": "1", +# "vlanshowinfo-media-type": "enet", "vlanshowinfo-vlanmode": "ce-vlan"}, {"vlanshowinfo-vlanid": "5", "vlanshowinfo-media-type": "enet", "vlanshowinfo- +# vlanmode": "ce-vlan"}, {"vlanshowinfo-vlanid": "6", "vlanshowinfo-media-type": "enet", "vlanshowinfo-vlanmode": "ce-vlan"}, {"vlanshowinfo-vlanid": "7" +# , "vlanshowinfo-media-type": "enet", "vlanshowinfo-vlanmode": "ce-vlan"}]}} +# +# nxos-9k# show running-config | section ^vlan +# vlan 1,5-7 +# vlan 5 +# state suspend +# name vlan5 +# vlan 7 +# shutdown +# name vlan7 +# vn-segment 190 + +- name: Gather vlans facts from the device using nxos_vlans + cisco.nxos.nxos_vlans: + state: gathered + +# Task output (redacted) +# ----------------------- +# gathered: +# - vlan_id: 5 +# enabled: True +# mode: "ce" +# name: "vlan5" +# state: suspend +# +# - vlan_id: 6 +# enabled: True +# mode: "ce" +# state: active +# +# - vlan_id: 7 +# enabled: False +# mode: "ce" +# name: "vlan7" +# state: active +# mapped_vni: 190 +""" +RETURN = """ +before: + description: The configuration as structured data prior to module invocation. + returned: always + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +after: + description: The configuration as structured data after module completion. + returned: when changed + type: list + sample: > + The configuration returned will always be in the same format + of the parameters above. +commands: + description: The set of commands pushed to the remote device. + returned: always + type: list + sample: ['vlan 5', 'name test-vlan5', 'state suspend'] +""" + + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.vlans.vlans import ( + VlansArgs, +) +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.config.vlans.vlans import ( + Vlans, +) + + +def main(): + """ + Main entry point for module execution + + :returns: the result form module invocation + """ + module = AnsibleModule(argument_spec=VlansArgs.argument_spec, supports_check_mode=True) + + result = Vlans(module).execute_module() + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc.py new file mode 100644 index 00000000..f660efc1 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc.py @@ -0,0 +1,470 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vpc +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages global VPC configuration +description: +- Manages global VPC configuration +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- The feature vpc must be enabled before this module can be used +- If not using management vrf, vrf must be globally on the device before using in + the pkl config +- Although source IP isn't required on the command line it is required when using + this module. The PKL VRF must also be configured prior to using this module. +- Both pkl_src and pkl_dest are needed when changing PKL VRF. +options: + domain: + description: + - VPC domain + required: true + type: str + role_priority: + description: + - Role priority for device. Remember lower is better. + type: str + system_priority: + description: + - System priority device. Remember they must match between peers. + type: str + pkl_src: + description: + - Source IP address used for peer keepalive link + type: str + pkl_dest: + description: + - Destination (remote) IP address used for peer keepalive link + - pkl_dest is required whenever pkl options are used. + type: str + pkl_vrf: + description: + - VRF used for peer keepalive link + - The VRF must exist on the device before using pkl_vrf. + - "(Note) 'default' is an overloaded term: Default vrf context for pkl_vrf is + 'management'; 'pkl_vrf: default' refers to the literal 'default' rib." + type: str + peer_gw: + description: + - Enables/Disables peer gateway + type: bool + auto_recovery: + description: + - Enables/Disables auto recovery on platforms that support disable + - timers are not modifiable with this attribute + - mutually exclusive with auto_recovery_reload_delay + type: bool + auto_recovery_reload_delay: + description: + - Manages auto-recovery reload-delay timer in seconds + - mutually exclusive with auto_recovery + type: str + delay_restore: + description: + - manages delay restore command and config value in seconds + type: str + delay_restore_interface_vlan: + description: + - manages delay restore interface-vlan command and config value in seconds + - not supported on all platforms + type: str + delay_restore_orphan_port: + description: + - manages delay restore orphan-port command and config value in seconds + - not supported on all platforms + type: str + state: + description: + - Manages desired state of the resource + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: configure a simple asn + cisco.nxos.nxos_vpc: + domain: 100 + role_priority: 1000 + system_priority: 2000 + pkl_dest: 192.168.100.4 + pkl_src: 10.1.100.20 + peer_gw: true + auto_recovery: true + +- name: configure + cisco.nxos.nxos_vpc: + domain: 100 + role_priority: 32667 + system_priority: 2000 + peer_gw: true + pkl_src: 10.1.100.2 + pkl_dest: 192.168.100.4 + auto_recovery: true + +- name: Configure VPC with delay restore and existing keepalive VRF + cisco.nxos.nxos_vpc: + domain: 10 + role_priority: 28672 + system_priority: 2000 + delay_restore: 180 + peer_gw: true + pkl_src: 1.1.1.2 + pkl_dest: 1.1.1.1 + pkl_vrf: vpckeepalive + auto_recovery: true +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["vpc domain 100", + "peer-keepalive destination 192.168.100.4 source 10.1.100.20 vrf management", + "auto-recovery", "peer-gateway"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, + run_commands, +) + + +CONFIG_ARGS = { + "role_priority": "role priority {role_priority}", + "system_priority": "system-priority {system_priority}", + "delay_restore": "delay restore {delay_restore}", + "delay_restore_interface_vlan": "delay restore interface-vlan {delay_restore_interface_vlan}", + "delay_restore_orphan_port": "delay restore orphan-port {delay_restore_orphan_port}", + "peer_gw": "{peer_gw} peer-gateway", + "auto_recovery": "{auto_recovery} auto-recovery", + "auto_recovery_reload_delay": "auto-recovery reload-delay {auto_recovery_reload_delay}", +} + +PARAM_TO_DEFAULT_KEYMAP = { + "delay_restore": "60", + "delay_restore_interface_vlan": "10", + "delay_restore_orphan_port": "0", + "role_priority": "32667", + "system_priority": "32667", + "peer_gw": False, + "auto_recovery_reload_delay": 240, +} + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_vrf_list(module): + try: + body = run_commands(module, ["show vrf all | json"])[0] + vrf_table = body["TABLE_vrf"]["ROW_vrf"] + except (KeyError, AttributeError): + return [] + + vrf_list = [] + if vrf_table: + for each in vrf_table: + vrf_list.append(str(each["vrf_name"].lower())) + + return vrf_list + + +def get_auto_recovery_default(module): + auto = False + data = run_commands(module, ["show inventory | json"])[0] + pid = data["TABLE_inv"]["ROW_inv"][0]["productid"] + if re.search(r"N7K", pid): + auto = True + elif re.search(r"N9K", pid): + data = run_commands(module, ["show hardware | json"])[0] + ver = data["kickstart_ver_str"] + if re.search(r"7.0\(3\)F", ver): + auto = True + + return auto + + +def get_vpc(module): + body = run_commands(module, ["show vpc | json"])[0] + if body: + domain = str(body["vpc-domain-id"]) + else: + body = run_commands(module, ["show run vpc | inc domain"])[0] + if body: + domain = body.split()[2] + else: + domain = "not configured" + + vpc = {} + if domain != "not configured": + run = get_config(module, flags=["vpc all"]) + if run: + vpc["domain"] = domain + for key in PARAM_TO_DEFAULT_KEYMAP.keys(): + vpc[key] = PARAM_TO_DEFAULT_KEYMAP.get(key) + vpc["auto_recovery"] = get_auto_recovery_default(module) + vpc_list = run.split("\n") + for each in vpc_list: + if "role priority" in each: + line = each.split() + vpc["role_priority"] = line[-1] + if "system-priority" in each: + line = each.split() + vpc["system_priority"] = line[-1] + if re.search(r"delay restore \d+", each): + line = each.split() + vpc["delay_restore"] = line[-1] + if "delay restore interface-vlan" in each: + line = each.split() + vpc["delay_restore_interface_vlan"] = line[-1] + if "delay restore orphan-port" in each: + line = each.split() + vpc["delay_restore_orphan_port"] = line[-1] + if "auto-recovery" in each: + vpc["auto_recovery"] = False if "no " in each else True + line = each.split() + vpc["auto_recovery_reload_delay"] = line[-1] + if "peer-gateway" in each: + vpc["peer_gw"] = False if "no " in each else True + if "peer-keepalive destination" in each: + # destination is reqd; src & vrf are optional + m = re.search( + r"destination (?P<pkl_dest>[\d.]+)" + r"(?:.* source (?P<pkl_src>[\d.]+))*" + r"(?:.* vrf (?P<pkl_vrf>\S+))*", + each, + ) + if m: + for pkl in m.groupdict().keys(): + if m.group(pkl): + vpc[pkl] = m.group(pkl) + return vpc + + +def pkl_dependencies(module, delta, existing): + """peer-keepalive dependency checking. + 1. 'destination' is required with all pkl configs. + 2. If delta has optional pkl keywords present, then all optional pkl + keywords in existing must be added to delta, otherwise the device cli + will remove those values when the new config string is issued. + 3. The desired behavior for this set of properties is to merge changes; + therefore if an optional pkl property exists on the device but not + in the playbook, then that existing property should be retained. + Example: + CLI: peer-keepalive dest 10.1.1.1 source 10.1.1.2 vrf orange + Playbook: {pkl_dest: 10.1.1.1, pkl_vrf: blue} + Result: peer-keepalive dest 10.1.1.1 source 10.1.1.2 vrf blue + """ + pkl_existing = [i for i in existing.keys() if i.startswith("pkl")] + for pkl in pkl_existing: + param = module.params.get(pkl) + if not delta.get(pkl): + if param and param == existing[pkl]: + # delta is missing this param because it's idempotent; + # however another pkl command has changed; therefore + # explicitly add it to delta so that the cli retains it. + delta[pkl] = existing[pkl] + elif param is None and existing[pkl]: + # retain existing pkl commands even if not in playbook + delta[pkl] = existing[pkl] + + +def get_commands_to_config_vpc(module, vpc, domain, existing): + vpc = dict(vpc) + + domain_only = vpc.get("domain") + + commands = [] + if "pkl_dest" in vpc: + pkl_command = "peer-keepalive destination {pkl_dest}".format(**vpc) + if "pkl_src" in vpc: + pkl_command += " source {pkl_src}".format(**vpc) + if "pkl_vrf" in vpc: + pkl_command += " vrf {pkl_vrf}".format(**vpc) + commands.append(pkl_command) + + if "auto_recovery" in vpc: + if not vpc.get("auto_recovery"): + vpc["auto_recovery"] = "no" + else: + vpc["auto_recovery"] = "" + + if "peer_gw" in vpc: + if not vpc.get("peer_gw"): + vpc["peer_gw"] = "no" + else: + vpc["peer_gw"] = "" + + for param in vpc: + command = CONFIG_ARGS.get(param) + if command is not None: + command = command.format(**vpc).strip() + if "peer-gateway" in command: + commands.append("terminal dont-ask") + commands.append(command) + + if commands or domain_only: + commands.insert(0, "vpc domain {0}".format(domain)) + return commands + + +def main(): + argument_spec = dict( + domain=dict(required=True, type="str"), + role_priority=dict(required=False, type="str"), + system_priority=dict(required=False, type="str"), + pkl_src=dict(required=False), + pkl_dest=dict(required=False), + pkl_vrf=dict(required=False), + peer_gw=dict(required=False, type="bool"), + auto_recovery=dict(required=False, type="bool"), + auto_recovery_reload_delay=dict(required=False, type="str"), + delay_restore=dict(required=False, type="str"), + delay_restore_interface_vlan=dict(required=False, type="str"), + delay_restore_orphan_port=dict(required=False, type="str"), + state=dict(choices=["absent", "present"], default="present"), + ) + + mutually_exclusive = [("auto_recovery", "auto_recovery_reload_delay")] + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + results = {"changed": False, "warnings": warnings} + + domain = module.params["domain"] + role_priority = module.params["role_priority"] + system_priority = module.params["system_priority"] + pkl_src = module.params["pkl_src"] + pkl_dest = module.params["pkl_dest"] + pkl_vrf = module.params["pkl_vrf"] + peer_gw = module.params["peer_gw"] + auto_recovery = module.params["auto_recovery"] + auto_recovery_reload_delay = module.params["auto_recovery_reload_delay"] + delay_restore = module.params["delay_restore"] + delay_restore_interface_vlan = module.params["delay_restore_interface_vlan"] + delay_restore_orphan_port = module.params["delay_restore_orphan_port"] + state = module.params["state"] + + args = dict( + domain=domain, + role_priority=role_priority, + system_priority=system_priority, + pkl_src=pkl_src, + pkl_dest=pkl_dest, + pkl_vrf=pkl_vrf, + peer_gw=peer_gw, + auto_recovery=auto_recovery, + auto_recovery_reload_delay=auto_recovery_reload_delay, + delay_restore=delay_restore, + delay_restore_interface_vlan=delay_restore_interface_vlan, + delay_restore_orphan_port=delay_restore_orphan_port, + ) + + if not pkl_dest: + if pkl_src: + module.fail_json(msg="dest IP for peer-keepalive is required" " when src IP is present") + elif pkl_vrf: + if pkl_vrf != "management": + module.fail_json( + msg="dest and src IP for peer-keepalive are required" " when vrf is present", + ) + else: + module.fail_json( + msg="dest IP for peer-keepalive is required" " when vrf is present", + ) + if pkl_vrf: + if pkl_vrf.lower() not in get_vrf_list(module): + module.fail_json( + msg="The VRF you are trying to use for the peer " + "keepalive link is not on device yet. Add it" + " first, please.", + ) + proposed = dict((k, v) for k, v in args.items() if v is not None) + existing = get_vpc(module) + + commands = [] + if state == "present": + delta = {} + for key, value in proposed.items(): + if str(value).lower() == "default" and key != "pkl_vrf": + # 'default' is a reserved word for vrf + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if existing.get(key) != value: + delta[key] = value + + if delta: + pkl_dependencies(module, delta, existing) + command = get_commands_to_config_vpc(module, delta, domain, existing) + commands.append(command) + elif state == "absent": + if existing: + if domain != existing["domain"]: + module.fail_json( + msg="You are trying to remove a domain that " "does not exist on the device", + ) + else: + commands.append("terminal dont-ask") + commands.append("no vpc domain {0}".format(domain)) + + cmds = flatten_list(commands) + results["commands"] = cmds + + if cmds: + results["changed"] = True + if not module.check_mode: + load_config(module, cmds) + if "configure" in cmds: + cmds.pop(0) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc_interface.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc_interface.py new file mode 100644 index 00000000..5e318457 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vpc_interface.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vpc_interface +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages interface VPC configuration +description: +- Manages interface VPC configuration +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Either vpc or peer_link param is required, but not both. +- C(state=absent) removes whatever VPC config is on a port-channel if one exists. +- Re-assigning a vpc or peerlink from one portchannel to another is not supported. The + module will force the user to unconfigure an existing vpc/pl before configuring + the same value on a new portchannel +options: + portchannel: + description: + - Group number of the portchannel that will be configured. + required: true + type: str + vpc: + description: + - VPC group/id that will be configured on associated portchannel. + type: str + peer_link: + description: + - Set to true/false for peer link config on associated portchannel. + type: bool + state: + description: + - Manages desired state of the resource. + choices: + - present + - absent + default: present + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_vpc_interface: + portchannel: 10 + vpc: 100 +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface port-channel100", "vpc 10"] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, + run_commands, +) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_portchannel_list(module): + portchannels = [] + pc_list = [] + + try: + body = run_commands(module, ["show port-channel summary | json"])[0] + pc_list = body["TABLE_channel"]["ROW_channel"] + except (KeyError, AttributeError, TypeError): + return portchannels + + if pc_list: + if isinstance(pc_list, dict): + pc_list = [pc_list] + + for pc in pc_list: + portchannels.append(pc["group"]) + + return portchannels + + +def get_existing_portchannel_to_vpc_mappings(module): + pc_vpc_mapping = {} + + try: + body = run_commands(module, ["show vpc brief | json"])[0] + vpc_table = body["TABLE_vpc"]["ROW_vpc"] + except (KeyError, AttributeError, TypeError): + vpc_table = None + + if vpc_table: + if isinstance(vpc_table, dict): + vpc_table = [vpc_table] + + for vpc in vpc_table: + pc_vpc_mapping[str(vpc["vpc-id"])] = str(vpc["vpc-ifindex"]) + + return pc_vpc_mapping + + +def peer_link_exists(module): + found = False + run = get_config(module, flags=["vpc"]) + + vpc_list = run.split("\n") + for each in vpc_list: + if "peer-link" in each: + found = True + return found + + +def get_active_vpc_peer_link(module): + peer_link = None + + try: + body = run_commands(module, ["show vpc brief | json"])[0] + peer_link = body["TABLE_peerlink"]["ROW_peerlink"]["peerlink-ifindex"] + except (KeyError, AttributeError, TypeError): + return peer_link + + return peer_link + + +def get_portchannel_vpc_config(module, portchannel): + peer_link_pc = None + peer_link = False + vpc = "" + pc = "" + config = {} + + try: + body = run_commands(module, ["show vpc brief | json"])[0] + table = body["TABLE_peerlink"]["ROW_peerlink"] + except (KeyError, AttributeError, TypeError): + table = {} + + if table: + peer_link_pc = table.get("peerlink-ifindex", None) + + if peer_link_pc: + plpc = str(peer_link_pc[2:]) + if portchannel == plpc: + config["portchannel"] = portchannel + config["peer-link"] = True + config["vpc"] = vpc + + mapping = get_existing_portchannel_to_vpc_mappings(module) + + for existing_vpc, port_channel in mapping.items(): + port_ch = str(port_channel[2:]) + if port_ch == portchannel: + pc = port_ch + vpc = str(existing_vpc) + + config["portchannel"] = pc + config["peer-link"] = peer_link + config["vpc"] = vpc + + return config + + +def get_commands_to_config_vpc_interface(portchannel, delta, config_value, existing): + commands = [] + + if not delta.get("peer-link") and existing.get("peer-link"): + commands.append("no vpc peer-link") + commands.insert(0, "interface port-channel{0}".format(portchannel)) + + elif delta.get("peer-link") and not existing.get("peer-link"): + commands.append("vpc peer-link") + commands.insert(0, "interface port-channel{0}".format(portchannel)) + + elif delta.get("vpc") and not existing.get("vpc"): + command = "vpc {0}".format(config_value) + commands.append(command) + commands.insert(0, "interface port-channel{0}".format(portchannel)) + + return commands + + +def state_present(portchannel, delta, config_value, existing): + commands = [] + + command = get_commands_to_config_vpc_interface(portchannel, delta, config_value, existing) + commands.append(command) + + return commands + + +def state_absent(portchannel, existing): + commands = [] + if existing.get("vpc"): + command = "no vpc" + commands.append(command) + elif existing.get("peer-link"): + command = "no vpc peer-link" + commands.append(command) + if commands: + commands.insert(0, "interface port-channel{0}".format(portchannel)) + + return commands + + +def main(): + argument_spec = dict( + portchannel=dict(required=True, type="str"), + vpc=dict(required=False, type="str"), + peer_link=dict(required=False, type="bool"), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=[["vpc", "peer_link"]], + supports_check_mode=True, + ) + + warnings = list() + commands = [] + results = {"changed": False, "warnings": warnings} + + portchannel = module.params["portchannel"] + vpc = module.params["vpc"] + peer_link = module.params["peer_link"] + state = module.params["state"] + + args = {"portchannel": portchannel, "vpc": vpc, "peer-link": peer_link} + active_peer_link = None + + if portchannel not in get_portchannel_list(module): + if not portchannel.isdigit() or int(portchannel) not in get_portchannel_list(module): + module.fail_json( + msg="The portchannel you are trying to make a" + " VPC or PL is not created yet. " + "Create it first!", + ) + if vpc: + mapping = get_existing_portchannel_to_vpc_mappings(module) + + if vpc in mapping and portchannel != mapping[vpc].strip("Po"): + module.fail_json( + msg="This vpc is already configured on " + "another portchannel. Remove it first " + "before trying to assign it here. ", + existing_portchannel=mapping[vpc], + ) + + for vpcid, existing_pc in mapping.items(): + if portchannel == existing_pc.strip("Po") and vpcid != vpc: + module.fail_json( + msg="This portchannel already has another" + " VPC configured. Remove it first " + "before assigning this one", + existing_vpc=vpcid, + ) + + if peer_link_exists(module): + active_peer_link = get_active_vpc_peer_link(module) + if active_peer_link[-2:] == portchannel: + module.fail_json( + msg="That port channel is the current " + "PEER LINK. Remove it if you want it" + " to be a VPC", + ) + config_value = vpc + + elif peer_link is not None: + if peer_link_exists(module): + active_peer_link = get_active_vpc_peer_link(module)[2::] + if active_peer_link != portchannel: + if peer_link: + module.fail_json( + msg="A peer link already exists on" " the device. Remove it first", + current_peer_link="Po{0}".format(active_peer_link), + ) + config_value = "peer-link" + + proposed = dict((k, v) for k, v in args.items() if v is not None) + existing = get_portchannel_vpc_config(module, portchannel) + + if state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + commands = state_present(portchannel, delta, config_value, existing) + + elif state == "absent" and existing: + commands = state_absent(portchannel, existing) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + load_config(module, cmds) + results["changed"] = True + if "configure" in cmds: + cmds.pop(0) + + results["commands"] = cmds + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf.py new file mode 100644 index 00000000..66fa43ab --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf.py @@ -0,0 +1,616 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vrf +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages global VRF configuration. +description: +- This module provides declarative management of VRFs on CISCO NXOS network devices. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +- Trishna Guha (@trishnaguha) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Cisco NX-OS creates the default VRF by itself. Therefore, you're not allowed to + use default as I(vrf) name in this module. +- C(vrf) name must be shorter than 32 chars. +- VRF names are not case sensible in NX-OS. Anyway, the name is stored just like it's + inserted by the user and it'll not be changed again unless the VRF is removed and + re-created. i.e. C(vrf=NTC) will create a VRF named NTC, but running it again with + C(vrf=ntc) will not cause a configuration change. +options: + name: + description: + - Name of VRF to be managed. + aliases: + - vrf + type: str + admin_state: + description: + - Administrative state of the VRF. + default: up + choices: + - up + - down + type: str + vni: + description: + - Specify virtual network identifier. Valid values are Integer or keyword 'default'. + type: str + rd: + description: + - VPN Route Distinguisher (RD). Valid values are a string in one of the route-distinguisher + formats (ASN2:NN, ASN4:NN, or IPV4:NN); the keyword 'auto', or the keyword 'default'. + type: str + interfaces: + description: + - List of interfaces to check the VRF has been configured correctly or keyword + 'default'. + type: list + elements: str + associated_interfaces: + description: + - This is a intent option and checks the operational state of the for given vrf + C(name) for associated interfaces. If the value in the C(associated_interfaces) + does not match with the operational state of vrf interfaces on device it will + result in failure. + type: list + elements: str + aggregate: + description: List of VRFs definitions. + type: list + elements: dict + suboptions: + name: + description: + - Name of VRF to be managed. + aliases: + - vrf + type: str + admin_state: + description: + - Administrative state of the VRF. + choices: + - up + - down + type: str + vni: + description: + - Specify virtual network identifier. Valid values are Integer or keyword 'default'. + type: str + rd: + description: + - VPN Route Distinguisher (RD). Valid values are a string in one of the route-distinguisher + formats (ASN2:NN, ASN4:NN, or IPV4:NN); the keyword 'auto', or the keyword 'default'. + type: str + interfaces: + description: + - List of interfaces to check the VRF has been configured correctly or keyword + 'default'. + type: list + elements: str + associated_interfaces: + description: + - This is a intent option and checks the operational state of the for given vrf + C(name) for associated interfaces. If the value in the C(associated_interfaces) + does not match with the operational state of vrf interfaces on device it will + result in failure. + type: list + elements: str + state: + description: + - Manages desired state of the resource. + choices: + - present + - absent + type: str + description: + description: + - Description of the VRF or keyword 'default'. + type: str + delay: + description: + - Time in seconds to wait before checking for the operational state on remote + device. This wait is applicable for operational state arguments. + type: int + purge: + description: + - Purge VRFs not defined in the I(aggregate) parameter. + type: bool + default: no + state: + description: + - Manages desired state of the resource. + default: present + choices: + - present + - absent + type: str + description: + description: + - Description of the VRF or keyword 'default'. + type: str + delay: + description: + - Time in seconds to wait before checking for the operational state on remote + device. This wait is applicable for operational state arguments. + default: 10 + type: int +""" + +EXAMPLES = """ +- name: Ensure ntc VRF exists on switch + cisco.nxos.nxos_vrf: + name: ntc + description: testing + state: present + +- name: Aggregate definition of VRFs + cisco.nxos.nxos_vrf: + aggregate: + - {name: test1, description: Testing, admin_state: down} + - {name: test2, interfaces: Ethernet1/2} + +- name: Aggregate definitions of VRFs with Purge + cisco.nxos.nxos_vrf: + aggregate: + - {name: ntc1, description: purge test1} + - {name: ntc2, description: purge test2} + state: present + purge: yes + +- name: Delete VRFs exist on switch + cisco.nxos.nxos_vrf: + aggregate: + - {name: ntc1} + - {name: ntc2} + state: absent + +- name: Assign interfaces to VRF declaratively + cisco.nxos.nxos_vrf: + name: test1 + interfaces: + - Ethernet2/3 + - Ethernet2/5 + +- name: Check interfaces assigned to VRF + cisco.nxos.nxos_vrf: + name: test1 + associated_interfaces: + - Ethernet2/3 + - Ethernet2/5 + +- name: Ensure VRF is tagged with interface Ethernet2/5 only (Removes from Ethernet2/3) + cisco.nxos.nxos_vrf: + name: test1 + interfaces: + - Ethernet2/5 + +- name: Delete VRF + cisco.nxos.nxos_vrf: + name: ntc + state: absent +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - vrf context ntc + - no shutdown + - interface Ethernet1/2 + - no switchport + - vrf member test2 +""" + +import re +import time + +from copy import deepcopy + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( + remove_default_spec, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_interface_type, + load_config, + run_commands, +) + + +def search_obj_in_list(name, lst): + for o in lst: + if o["name"] == name: + return o + + +def execute_show_command(command, module): + if "show run" not in command: + output = "json" + else: + output = "text" + cmds = [{"command": command, "output": output}] + body = run_commands(module, cmds) + return body + + +def get_existing_vrfs(module): + objs = list() + command = "show vrf all" + try: + body = execute_show_command(command, module)[0] + except IndexError: + return list() + try: + vrf_table = body["TABLE_vrf"]["ROW_vrf"] + except (TypeError, IndexError, KeyError): + return list() + + if isinstance(vrf_table, list): + for vrf in vrf_table: + obj = {} + obj["name"] = vrf["vrf_name"] + objs.append(obj) + + elif isinstance(vrf_table, dict): + obj = {} + obj["name"] = vrf_table["vrf_name"] + objs.append(obj) + + return objs + + +def map_obj_to_commands(updates, module): + commands = list() + want, have = updates + state = module.params["state"] + purge = module.params["purge"] + + args = ("rd", "description", "vni") + + for w in want: + name = w["name"] + admin_state = w["admin_state"] + vni = w["vni"] + interfaces = w.get("interfaces") or [] + if purge: + state = "absent" + else: + state = w["state"] + del w["state"] + + obj_in_have = search_obj_in_list(name, have) + if state == "absent" and obj_in_have: + commands.append("no vrf context {0}".format(name)) + + elif state == "present": + if not obj_in_have: + commands.append("vrf context {0}".format(name)) + for item in args: + candidate = w.get(item) + if candidate and candidate != "default": + cmd = item + " " + str(candidate) + commands.append(cmd) + if admin_state == "up": + commands.append("no shutdown") + elif admin_state == "down": + commands.append("shutdown") + commands.append("exit") + + if interfaces and interfaces[0] != "default": + for i in interfaces: + commands.append("interface {0}".format(i)) + if get_interface_type(i) in ( + "ethernet", + "portchannel", + ): + commands.append("no switchport") + commands.append("vrf member {0}".format(name)) + + else: + # If vni is already configured on vrf, unconfigure it first. + if vni: + if obj_in_have.get("vni") and vni != obj_in_have.get("vni"): + commands.append("no vni {0}".format(obj_in_have.get("vni"))) + + for item in args: + candidate = w.get(item) + if candidate == "default": + if obj_in_have.get(item): + cmd = "no " + item + " " + obj_in_have.get(item) + commands.append(cmd) + elif candidate and candidate != obj_in_have.get(item): + cmd = item + " " + str(candidate) + commands.append(cmd) + if admin_state and admin_state != obj_in_have.get("admin_state"): + if admin_state == "up": + commands.append("no shutdown") + elif admin_state == "down": + commands.append("shutdown") + + if commands: + commands.insert(0, "vrf context {0}".format(name)) + commands.append("exit") + + if interfaces and interfaces[0] != "default": + if not obj_in_have["interfaces"]: + for i in interfaces: + commands.append("vrf context {0}".format(name)) + commands.append("exit") + commands.append("interface {0}".format(i)) + if get_interface_type(i) in ( + "ethernet", + "portchannel", + ): + commands.append("no switchport") + commands.append("vrf member {0}".format(name)) + + elif set(interfaces) != set(obj_in_have["interfaces"]): + missing_interfaces = list(set(interfaces) - set(obj_in_have["interfaces"])) + for i in missing_interfaces: + commands.append("vrf context {0}".format(name)) + commands.append("exit") + commands.append("interface {0}".format(i)) + if get_interface_type(i) in ( + "ethernet", + "portchannel", + ): + commands.append("no switchport") + commands.append("vrf member {0}".format(name)) + + superfluous_interfaces = list( + set(obj_in_have["interfaces"]) - set(interfaces), + ) + for i in superfluous_interfaces: + commands.append("vrf context {0}".format(name)) + commands.append("exit") + commands.append("interface {0}".format(i)) + if get_interface_type(i) in ( + "ethernet", + "portchannel", + ): + commands.append("no switchport") + commands.append("no vrf member {0}".format(name)) + elif interfaces and interfaces[0] == "default": + if obj_in_have["interfaces"]: + for i in obj_in_have["interfaces"]: + commands.append("vrf context {0}".format(name)) + commands.append("exit") + commands.append("interface {0}".format(i)) + if get_interface_type(i) in ( + "ethernet", + "portchannel", + ): + commands.append("no switchport") + commands.append("no vrf member {0}".format(name)) + + if purge: + existing = get_existing_vrfs(module) + if existing: + for h in existing: + if h["name"] in ("default", "management"): + pass + else: + obj_in_want = search_obj_in_list(h["name"], want) + if not obj_in_want: + commands.append("no vrf context {0}".format(h["name"])) + + return commands + + +def validate_vrf(name, module): + if name: + name = name.strip() + if name == "default": + module.fail_json(msg="cannot use default as name of a VRF") + elif len(name) > 32: + module.fail_json(msg="VRF name exceeded max length of 32", name=name) + else: + return name + + +def map_params_to_obj(module): + obj = [] + aggregate = module.params.get("aggregate") + if aggregate: + for item in aggregate: + for key in item: + if item.get(key) is None: + item[key] = module.params[key] + + d = item.copy() + d["name"] = validate_vrf(d["name"], module) + obj.append(d) + else: + obj.append( + { + "name": validate_vrf(module.params["name"], module), + "description": module.params["description"], + "vni": module.params["vni"], + "rd": module.params["rd"], + "admin_state": module.params["admin_state"], + "state": module.params["state"], + "interfaces": module.params["interfaces"], + "associated_interfaces": module.params["associated_interfaces"], + }, + ) + return obj + + +def get_value(arg, config, module): + extra_arg_regex = re.compile(r"(?:{0}\s)(?P<value>.*)$".format(arg), re.M) + value = "" + if arg in config: + value = extra_arg_regex.search(config).group("value") + return value + + +def map_config_to_obj(want, element_spec, module): + objs = list() + + for w in want: + obj = deepcopy(element_spec) + del obj["delay"] + del obj["state"] + + command = "show vrf {0}".format(w["name"]) + try: + body = execute_show_command(command, module)[0] + vrf_table = body["TABLE_vrf"]["ROW_vrf"] + except (TypeError, IndexError): + return list() + + name = vrf_table["vrf_name"] + obj["name"] = name + obj["admin_state"] = vrf_table["vrf_state"].lower() + + command = "show run all | section vrf.context.{0}".format(name) + body = execute_show_command(command, module)[0] + extra_params = ["vni", "rd", "description"] + for param in extra_params: + obj[param] = get_value(param, body, module) + + obj["interfaces"] = [] + command = "show vrf {0} interface".format(name) + try: + body = execute_show_command(command, module)[0] + vrf_int = body["TABLE_if"]["ROW_if"] + except (TypeError, IndexError): + vrf_int = None + + if vrf_int: + if isinstance(vrf_int, list): + for i in vrf_int: + intf = i["if_name"] + obj["interfaces"].append(intf) + elif isinstance(vrf_int, dict): + intf = vrf_int["if_name"] + obj["interfaces"].append(intf) + + objs.append(obj) + return objs + + +def check_declarative_intent_params(want, module, element_spec, result): + have = None + is_delay = False + + for w in want: + if w.get("associated_interfaces") is None: + continue + + if result["changed"] and not is_delay: + time.sleep(module.params["delay"]) + is_delay = True + + if have is None: + have = map_config_to_obj(want, element_spec, module) + + for i in w["associated_interfaces"]: + obj_in_have = search_obj_in_list(w["name"], have) + + if obj_in_have: + interfaces = obj_in_have.get("interfaces") + if interfaces is not None and i not in interfaces: + module.fail_json(msg="Interface %s not configured on vrf %s" % (i, w["name"])) + + +def vrf_error_check(module, commands, responses): + """Checks for VRF config errors and executes a retry in some circumstances.""" + pattern = "ERROR: Deletion of VRF .* in progress" + if re.search(pattern, str(responses)): + # Allow delay/retry for VRF changes + time.sleep(15) + responses = load_config(module, commands, opts={"catch_clierror": True}) + if re.search(pattern, str(responses)): + module.fail_json(msg="VRF config (and retry) failure: %s " % responses) + module.warn("VRF config delayed by VRF deletion - passed on retry") + + +def main(): + """main entry point for module execution""" + element_spec = dict( + name=dict(type="str", aliases=["vrf"]), + description=dict(type="str"), + vni=dict(type="str"), + rd=dict(type="str"), + admin_state=dict(type="str", default="up", choices=["up", "down"]), + interfaces=dict(type="list", elements="str"), + associated_interfaces=dict(type="list", elements="str"), + delay=dict(type="int", default=10), + state=dict(type="str", default="present", choices=["present", "absent"]), + ) + + aggregate_spec = deepcopy(element_spec) + + # remove default in aggregate spec, to handle common arguments + remove_default_spec(aggregate_spec) + + argument_spec = dict( + aggregate=dict(type="list", elements="dict", options=aggregate_spec), + purge=dict(type="bool", default=False), + ) + + argument_spec.update(element_spec) + + required_one_of = [["name", "aggregate"]] + mutually_exclusive = [["name", "aggregate"]] + module = AnsibleModule( + argument_spec=argument_spec, + required_one_of=required_one_of, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + result = {"changed": False} + if warnings: + result["warnings"] = warnings + + want = map_params_to_obj(module) + have = map_config_to_obj(want, element_spec, module) + + commands = map_obj_to_commands((want, have), module) + result["commands"] = commands + + if commands and not module.check_mode: + responses = load_config(module, commands, opts={"catch_clierror": True}) + vrf_error_check(module, commands, responses) + result["changed"] = True + + check_declarative_intent_params(want, module, element_spec, result) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_af.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_af.py new file mode 100644 index 00000000..bf155ce8 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_af.py @@ -0,0 +1,274 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vrf_af +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VRF AF. +description: +- Manages VRF AF +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- Default, where supported, restores params default value. +- In case of C(state=absent) the address-family configuration will be absent. Therefore + the options C(route_target_both_auto_evpn) and C(route_targets) are ignored. +options: + vrf: + description: + - Name of the VRF. + required: true + type: str + afi: + description: + - Address-Family Identifier (AFI). + required: true + choices: + - ipv4 + - ipv6 + type: str + route_target_both_auto_evpn: + description: + - Enable/Disable the EVPN route-target 'auto' setting for both import and export + target communities. + type: bool + route_targets: + description: + - Specify the route-targets which should be imported and/or exported under the + AF. This argument accepts a list of dicts that specify the route-target, the + direction (import|export|both) and state of each route-target. Default direction + is C(direction=both). See examples. + suboptions: + rt: + description: + - Defines the route-target itself + required: true + type: str + direction: + description: + - Indicates the direction of the route-target (import|export|both) + choices: + - import + - export + - both + default: both + type: str + state: + description: + - Determines whether the route-target with the given direction should be present + or not on the device. + choices: + - present + - absent + default: present + type: str + elements: dict + type: list + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_target_both_auto_evpn: true + state: present + +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_targets: + - rt: 65000:1000 + direction: import + - rt: 65001:1000 + direction: import + +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_targets: + - rt: 65000:1000 + direction: import + - rt: 65001:1000 + state: absent + +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_targets: + - rt: 65000:1000 + direction: export + - rt: 65001:1000 + direction: export + +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_targets: + - rt: 65000:1000 + direction: export + state: absent + +- cisco.nxos.nxos_vrf_af: + vrf: ntc + afi: ipv4 + route_targets: + - rt: 65000:1000 + direction: both + state: present + - rt: 65001:1000 + direction: import + state: present + - rt: 65002:1000 + direction: both + state: absent +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["vrf context ntc", "address-family ipv4 unicast"] +""" +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + NetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_config, + load_config, +) + + +def match_current_rt(rt, direction, current, rt_commands): + command = "route-target %s %s" % (direction, rt.get("rt")) + match = re.findall(command, current, re.M) + want = bool(rt.get("state") != "absent") + if not match and want: + rt_commands.append(command) + elif match and not want: + rt_commands.append("no %s" % command) + return rt_commands + + +def main(): + argument_spec = dict( + vrf=dict(required=True), + afi=dict(required=True, choices=["ipv4", "ipv6"]), + route_target_both_auto_evpn=dict(required=False, type="bool"), + state=dict(choices=["present", "absent"], default="present"), + route_targets=dict( + type="list", + elements="dict", + options=dict( + rt=dict(type="str", required=True), + direction=dict(choices=["import", "export", "both"], default="both"), + state=dict(choices=["present", "absent"], default="present"), + ), + ), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + result = {"changed": False, "warnings": warnings} + + config_text = get_config(module) + config = NetworkConfig(indent=2, contents=config_text) + + path = [ + "vrf context %s" % module.params["vrf"], + "address-family %s unicast" % module.params["afi"], + ] + + try: + current = config.get_block_config(path) + except ValueError: + current = None + + commands = list() + if current and module.params["state"] == "absent": + commands.append("no address-family %s unicast" % module.params["afi"]) + + elif module.params["state"] == "present": + rt_commands = list() + + if not current: + commands.append("address-family %s unicast" % module.params["afi"]) + current = "" + + have_auto_evpn = "route-target both auto evpn" in current + if module.params["route_target_both_auto_evpn"] is not None: + want_auto_evpn = bool(module.params["route_target_both_auto_evpn"]) + if want_auto_evpn and not have_auto_evpn: + commands.append("route-target both auto evpn") + elif have_auto_evpn and not want_auto_evpn: + commands.append("no route-target both auto evpn") + + if module.params["route_targets"] is not None: + for rt in module.params["route_targets"]: + if rt.get("direction") == "both" or not rt.get("direction"): + platform = get_capabilities(module)["device_info"]["network_os_platform"] + if platform.startswith("N9K") and rt.get("rt") == "auto": + rt_commands = match_current_rt(rt, "both", current, rt_commands) + else: + rt_commands = match_current_rt(rt, "import", current, rt_commands) + rt_commands = match_current_rt(rt, "export", current, rt_commands) + else: + rt_commands = match_current_rt(rt, rt.get("direction"), current, rt_commands) + + if rt_commands: + commands.extend(rt_commands) + + if commands and current: + commands.insert(0, "address-family %s unicast" % module.params["afi"]) + + if commands: + commands.insert(0, "vrf context %s" % module.params["vrf"]) + if not module.check_mode: + load_config(module, commands) + result["changed"] = True + + result["commands"] = commands + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_interface.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_interface.py new file mode 100644 index 00000000..9e9a0cef --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrf_interface.py @@ -0,0 +1,267 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vrf_interface +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages interface specific VRF configuration. +description: +- Manages interface specific VRF configuration. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- VRF needs to be added globally with M(cisco.nxos.nxos_vrf) before adding a VRF to an interface. +- Remove a VRF from an interface will still remove all L3 attributes just as it does + from CLI. +- VRF is not read from an interface until IP address is configured on that interface. +options: + vrf: + description: + - Name of VRF to be managed. + required: true + type: str + interface: + description: + - Full name of interface to be managed, i.e. Ethernet1/1. + required: true + type: str + state: + description: + - Manages desired state of the resource. + required: false + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: Ensure vrf ntc exists on Eth1/1 + cisco.nxos.nxos_vrf_interface: + vrf: ntc + interface: Ethernet1/1 + state: present + +- name: Ensure ntc VRF does not exist on Eth1/1 + cisco.nxos.nxos_vrf_interface: + vrf: ntc + interface: Ethernet1/1 + state: absent +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface loopback16", "vrf member ntc"] +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_interface_type, + load_config, + normalize_interface, + run_commands, +) + + +def execute_show_command(command, module): + if "show run" not in command: + output = "json" + else: + output = "text" + cmds = [{"command": command, "output": output}] + return run_commands(module, cmds)[0] + + +def get_interface_mode(interface, intf_type, module): + command = "show interface {0}".format(interface) + interface = {} + mode = "unknown" + + if intf_type in ["ethernet", "portchannel"]: + body = execute_show_command(command, module) + try: + interface_table = body["TABLE_interface"]["ROW_interface"] + except KeyError: + return mode + + if interface_table and isinstance(interface_table, dict): + mode = str(interface_table.get("eth_mode", "layer3")) + if mode == "access" or mode == "trunk": + mode = "layer2" + else: + return mode + + elif intf_type == "loopback" or intf_type == "svi": + mode = "layer3" + return mode + + +def get_vrf_list(module): + command = "show vrf all" + vrf_list = [] + body = execute_show_command(command, module) + + try: + vrf_table = body["TABLE_vrf"]["ROW_vrf"] + except (KeyError, AttributeError): + return vrf_list + + for each in vrf_table: + vrf_list.append(str(each["vrf_name"])) + + return vrf_list + + +def get_interface_info(interface, module): + if not interface.startswith("loopback"): + interface = interface.capitalize() + + command = "show run interface {0}".format(interface) + vrf_regex = r".*vrf\s+member\s+(?P<vrf>\S+).*" + + try: + body = execute_show_command(command, module) + match_vrf = re.match(vrf_regex, body, re.DOTALL) + group_vrf = match_vrf.groupdict() + vrf = group_vrf["vrf"] + except (AttributeError, TypeError): + return "" + + return vrf + + +def is_default(interface, module): + command = "show run interface {0}".format(interface) + + try: + body = execute_show_command(command, module) + raw_list = body.split("\n") + if raw_list[-1].startswith("interface"): + return True + else: + return False + + except (KeyError, IndexError): + return "DNE" + + +def main(): + argument_spec = dict( + vrf=dict(required=True), + interface=dict(type="str", required=True), + state=dict(default="present", choices=["present", "absent"], required=False), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + vrf = module.params["vrf"] + interface = normalize_interface(module.params["interface"]) + state = module.params["state"] + + device_info = get_capabilities(module) + network_api = device_info.get("network_api", "nxapi") + + current_vrfs = get_vrf_list(module) + if vrf not in current_vrfs: + warnings.append("The VRF is not present/active on the device. Use nxos_vrf to fix this.") + + intf_type = get_interface_type(interface) + if intf_type != "ethernet" and network_api == "cliconf": + if is_default(interface, module) == "DNE": + module.fail_json( + msg="interface does not exist on switch. Verify " + "switch platform or create it first with " + "nxos_interfaces if it's a logical interface", + ) + + mode = get_interface_mode(interface, intf_type, module) + if mode == "layer2": + module.fail_json( + msg="Ensure interface is a Layer 3 port before " + "configuring a VRF on an interface. You can " + "use nxos_interfaces", + ) + + current_vrf = get_interface_info(interface, module) + existing = dict(interface=interface, vrf=current_vrf) + changed = False + + if not existing["vrf"]: + pass + elif vrf != existing["vrf"] and state == "absent": + module.fail_json( + msg="The VRF you are trying to remove " + "from the interface does not exist " + "on that interface.", + interface=interface, + proposed_vrf=vrf, + existing_vrf=existing["vrf"], + ) + + commands = [] + if existing: + if state == "absent": + if existing and vrf == existing["vrf"]: + command = "no vrf member {0}".format(vrf) + commands.append(command) + + elif state == "present": + if existing["vrf"] != vrf: + command = "vrf member {0}".format(vrf) + commands.append(command) + + if commands: + commands.insert(0, "interface {0}".format(interface)) + + if commands: + if module.check_mode: + module.exit_json(changed=True, commands=commands) + else: + load_config(module, commands) + changed = True + if "configure" in commands: + commands.pop(0) + + results["commands"] = commands + results["changed"] = changed + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vrrp.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrrp.py new file mode 100644 index 00000000..b5a026c4 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vrrp.py @@ -0,0 +1,432 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vrrp +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VRRP configuration on NX-OS switches. +description: +- Manages VRRP configuration on NX-OS switches. +version_added: 1.0.0 +author: +- Jason Edelman (@jedelman8) +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- VRRP feature needs to be enabled first on the system. +- SVIs must exist before using this module. +- Interface must be a L3 port before using this module. +- C(state=absent) removes the VRRP group if it exists on the device. +- VRRP cannot be configured on loopback interfaces. +options: + group: + description: + - VRRP group number. + required: true + type: str + interface: + description: + - Full name of interface that is being managed for VRRP. + required: true + type: str + interval: + description: + - Time interval between advertisement or 'default' keyword + required: false + type: str + priority: + description: + - VRRP priority or 'default' keyword + type: str + preempt: + description: + - Enable/Disable preempt. + type: bool + vip: + description: + - VRRP virtual IP address or 'default' keyword + type: str + authentication: + description: + - Clear text authentication string or 'default' keyword + type: str + admin_state: + description: + - Used to enable or disable the VRRP process. + choices: + - shutdown + - no shutdown + - default + default: shutdown + type: str + state: + description: + - Specify desired state of the resource. + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +- name: Ensure vrrp group 100 and vip 10.1.100.1 is on vlan10 + cisco.nxos.nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + +- name: Ensure removal of the vrrp group config + cisco.nxos.nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + state: absent + +- name: Re-config with more params + cisco.nxos.nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + preempt: false + priority: 130 + authentication: AUTHKEY +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface vlan10", "vrrp 150", "address 10.1.15.1", + "authentication text testing", "no shutdown"] +""" + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + get_interface_type, + load_config, + run_commands, +) + + +PARAM_TO_DEFAULT_KEYMAP = { + "priority": "100", + "interval": "1", + "vip": "0.0.0.0", + "admin_state": "shutdown", +} + + +def execute_show_command(command, module): + if "show run" not in command: + output = "json" + else: + output = "text" + + commands = [{"command": command, "output": output}] + return run_commands(module, commands)[0] + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + if value: + new_dict[new_key] = str(value) + else: + new_dict[new_key] = value + return new_dict + + +def is_default(interface, module): + command = "show run interface {0}".format(interface) + + try: + body = execute_show_command(command, module) + if "invalid" in body.lower(): + return "DNE" + else: + raw_list = body.split("\n") + if raw_list[-1].startswith("interface"): + return True + else: + return False + except KeyError: + return "DNE" + + +def get_interface_mode(interface, intf_type, module): + command = "show interface {0}".format(interface) + interface = {} + mode = "unknown" + body = execute_show_command(command, module) + interface_table = body["TABLE_interface"]["ROW_interface"] + name = interface_table.get("interface") + + if intf_type in ["ethernet", "portchannel"]: + mode = str(interface_table.get("eth_mode", "layer3")) + + if mode == "access" or mode == "trunk": + mode = "layer2" + elif intf_type == "svi": + mode = "layer3" + + return mode, name + + +def get_vrr_status(group, module, interface): + command = "show run all | section interface.{0}$".format(interface) + body = execute_show_command(command, module) + vrf_index = None + admin_state = "shutdown" + + if body: + splitted_body = body.splitlines() + for index in range(0, len(splitted_body) - 1): + if splitted_body[index].strip() == "vrrp {0}".format(group): + vrf_index = index + vrf_section = splitted_body[vrf_index::] + + for line in vrf_section: + if line.strip() == "no shutdown": + admin_state = "no shutdown" + break + + return admin_state + + +def get_existing_vrrp(interface, group, module, name): + command = "show vrrp detail interface {0}".format(interface) + body = execute_show_command(command, module) + vrrp = {} + + vrrp_key = { + "sh_group_id": "group", + "sh_vip_addr": "vip", + "sh_priority": "priority", + "sh_group_preempt": "preempt", + "sh_auth_text": "authentication", + "sh_adv_interval": "interval", + } + + try: + vrrp_table = body["TABLE_vrrp_group"] + except (AttributeError, IndexError, TypeError): + return {} + + if isinstance(vrrp_table, dict): + vrrp_table = [vrrp_table] + + for each_vrrp in vrrp_table: + vrrp_row = each_vrrp["ROW_vrrp_group"] + parsed_vrrp = apply_key_map(vrrp_key, vrrp_row) + + if parsed_vrrp["preempt"] == "Disable": + parsed_vrrp["preempt"] = False + elif parsed_vrrp["preempt"] == "Enable": + parsed_vrrp["preempt"] = True + + if parsed_vrrp["group"] == group: + parsed_vrrp["admin_state"] = get_vrr_status(group, module, name) + return parsed_vrrp + + return vrrp + + +def get_commands_config_vrrp(delta, existing, group): + commands = [] + + CMDS = { + "priority": "priority {0}", + "preempt": "preempt", + "vip": "address {0}", + "interval": "advertisement-interval {0}", + "auth": "authentication text {0}", + "admin_state": "{0}", + } + + for arg in ["vip", "priority", "interval", "admin_state"]: + val = delta.get(arg) + if val == "default": + val = PARAM_TO_DEFAULT_KEYMAP.get(arg) + if val != existing.get(arg): + commands.append((CMDS.get(arg)).format(val)) + elif val: + commands.append((CMDS.get(arg)).format(val)) + + preempt = delta.get("preempt") + auth = delta.get("authentication") + + if preempt: + commands.append(CMDS.get("preempt")) + elif preempt is False: + commands.append("no " + CMDS.get("preempt")) + if auth: + if auth != "default": + commands.append((CMDS.get("auth")).format(auth)) + elif existing.get("authentication"): + commands.append("no authentication") + + if commands: + commands.insert(0, "vrrp {0}".format(group)) + + return commands + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def validate_params(param, module): + value = module.params[param] + + if param == "group": + try: + if int(value) < 1 or int(value) > 255: + raise ValueError + except ValueError: + module.fail_json( + msg="Warning! 'group' must be an integer between" " 1 and 255", + group=value, + ) + elif param == "priority": + try: + if int(value) < 1 or int(value) > 254: + raise ValueError + except ValueError: + module.fail_json( + msg="Warning! 'priority' must be an integer " "between 1 and 254", + priority=value, + ) + + +def main(): + argument_spec = dict( + group=dict(required=True, type="str"), + interface=dict(required=True), + interval=dict(required=False, type="str"), + priority=dict(required=False, type="str"), + preempt=dict(required=False, type="bool"), + vip=dict(required=False, type="str"), + admin_state=dict( + required=False, + type="str", + choices=["shutdown", "no shutdown", "default"], + default="shutdown", + ), + authentication=dict(required=False, type="str", no_log=True), + state=dict(choices=["absent", "present"], required=False, default="present"), + ) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + results = {"changed": False, "commands": [], "warnings": warnings} + + state = module.params["state"] + interface = module.params["interface"].lower() + group = module.params["group"] + priority = module.params["priority"] + interval = module.params["interval"] + preempt = module.params["preempt"] + vip = module.params["vip"] + authentication = module.params["authentication"] + admin_state = module.params["admin_state"] + + device_info = get_capabilities(module) + network_api = device_info.get("network_api", "nxapi") + + if state == "present" and not vip: + module.fail_json(msg='the "vip" param is required when state=present') + + intf_type = get_interface_type(interface) + if intf_type != "ethernet" and network_api == "cliconf": + if is_default(interface, module) == "DNE": + module.fail_json( + msg="That interface does not exist yet. Create " "it first.", + interface=interface, + ) + if intf_type == "loopback": + module.fail_json( + msg="Loopback interfaces don't support VRRP.", + interface=interface, + ) + + mode, name = get_interface_mode(interface, intf_type, module) + if mode == "layer2": + module.fail_json( + msg="That interface is a layer2 port.\nMake it " "a layer 3 port first.", + interface=interface, + ) + + args = dict( + group=group, + priority=priority, + preempt=preempt, + vip=vip, + authentication=authentication, + interval=interval, + admin_state=admin_state, + ) + + proposed = dict((k, v) for k, v in args.items() if v is not None) + existing = get_existing_vrrp(interface, group, module, name) + + commands = [] + + if state == "present": + delta = dict(set(proposed.items()).difference(existing.items())) + if delta: + command = get_commands_config_vrrp(delta, existing, group) + if command: + commands.append(command) + elif state == "absent": + if existing: + commands.append(["no vrrp {0}".format(group)]) + + if commands: + commands.insert(0, ["interface {0}".format(interface)]) + commands = flatten_list(commands) + results["commands"] = commands + results["changed"] = True + if not module.check_mode: + load_config(module, commands) + if "configure" in commands: + commands.pop(0) + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vsan.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vsan.py new file mode 100644 index 00000000..d95d95a9 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vsan.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_vsan +short_description: Configuration of vsan for Cisco NXOS MDS Switches. +description: +- Configuration of vsan for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + vsan: + description: + - List of vsan details to be added or removed + type: list + elements: dict + suboptions: + id: + description: + - Vsan id + required: true + type: int + name: + description: + - Name of the vsan + type: str + suspend: + description: + - suspend the vsan if True + type: bool + remove: + description: + - Removes the vsan if True + type: bool + interface: + description: + - List of vsan's interfaces to be added + type: list + elements: str +""" + +EXAMPLES = """ +- name: Test that vsan module works + cisco.nxos.nxos_vsan: + vsan: + - id: 922 + interface: + - fc1/1 + - fc1/2 + - port-channel 1 + name: vsan-SAN-A + remove: false + suspend: false + - id: 923 + interface: + - fc1/11 + - fc1/21 + - port-channel 2 + name: vsan-SAN-B + remove: false + suspend: true + - id: 1923 + name: vsan-SAN-Old + remove: true +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - vsan database + - vsan 922 interface fc1/40 + - vsan 922 interface port-channel 155 + - no terminal dont-ask +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + + +class Vsan(object): + def __init__(self, vsanid): + self.vsanid = vsanid + self.vsanname = None + self.vsanstate = None + self.vsanoperstate = None + self.vsaninterfaces = [] + + +class GetVsanInfoFromSwitch(object): + """docstring for GetVsanInfoFromSwitch""" + + def __init__(self, module): + self.module = module + self.vsaninfo = {} + self.processShowVsan() + self.processShowVsanMembership() + + def execute_show_vsan_cmd(self): + output = execute_show_command("show vsan", self.module)[0] + return output + + def execute_show_vsan_mem_cmd(self): + output = execute_show_command("show vsan membership", self.module)[0] + return output + + def processShowVsan(self): + patv = r"^vsan\s+(\d+)\s+information" + patnamestate = "name:(.*)state:(.*)" + patoperstate = "operational state:(.*)" + + output = self.execute_show_vsan_cmd().split("\n") + for o in output: + z = re.match(patv, o.strip()) + if z: + v = z.group(1).strip() + self.vsaninfo[v] = Vsan(v) + + z1 = re.match(patnamestate, o.strip()) + if z1: + n = z1.group(1).strip() + s = z1.group(2).strip() + self.vsaninfo[v].vsanname = n + self.vsaninfo[v].vsanstate = s + + z2 = re.match(patoperstate, o.strip()) + if z2: + oper = z2.group(1).strip() + self.vsaninfo[v].vsanoperstate = oper + + # 4094/4079 vsan is always present + self.vsaninfo["4079"] = Vsan("4079") + self.vsaninfo["4094"] = Vsan("4094") + + def processShowVsanMembership(self): + patv = r"^vsan\s+(\d+).*" + output = self.execute_show_vsan_mem_cmd().split("\n") + memlist = [] + v = None + for o in output: + z = re.match(patv, o.strip()) + if z: + if v is not None: + self.vsaninfo[v].vsaninterfaces = memlist + memlist = [] + v = z.group(1) + if "interfaces" not in o: + llist = o.strip().split() + memlist = memlist + llist + self.vsaninfo[v].vsaninterfaces = memlist + + def getVsanInfoObjects(self): + return self.vsaninfo + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + return run_commands(module, commands) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def main(): + vsan_element_spec = dict( + id=dict(required=True, type="int"), + name=dict(type="str"), + remove=dict(type="bool"), + suspend=dict(type="bool"), + interface=dict(type="list", elements="str"), + ) + + argument_spec = dict(vsan=dict(type="list", elements="dict", options=vsan_element_spec)) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + warnings = list() + messages = list() + commands_executed = list() + result = {"changed": False} + + obj = GetVsanInfoFromSwitch(module) + dictSwVsanObjs = obj.getVsanInfoObjects() + + commands = [] + vsan_list = module.params["vsan"] + + for eachvsan in vsan_list: + vsanid = str(eachvsan["id"]) + vsanname = eachvsan["name"] + vsanremove = eachvsan["remove"] + vsansuspend = eachvsan["suspend"] + vsaninterface_list = eachvsan["interface"] + + if int(vsanid) < 1 or int(vsanid) >= 4095: + module.fail_json( + msg=vsanid + " - This is an invalid vsan. Supported vsan range is 1-4094", + ) + + if vsanid in dictSwVsanObjs.keys(): + sw_vsanid = vsanid + sw_vsanname = dictSwVsanObjs[vsanid].vsanname + sw_vsanstate = dictSwVsanObjs[vsanid].vsanstate + sw_vsaninterfaces = dictSwVsanObjs[vsanid].vsaninterfaces + else: + sw_vsanid = None + sw_vsanname = None + sw_vsanstate = None + sw_vsaninterfaces = [] + + if vsanremove: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, hence cannot be removed") + continue + if vsanid == sw_vsanid: + commands.append("no vsan " + str(vsanid)) + messages.append("deleting the vsan " + str(vsanid)) + else: + messages.append( + "There is no vsan " + + str(vsanid) + + " present in the switch. Hence there is nothing to delete", + ) + continue + else: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append( + str(vsanid) + " is a reserved vsan, and always present on the switch", + ) + else: + if vsanid == sw_vsanid: + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch. Hence there is nothing to configure", + ) + else: + commands.append("vsan " + str(vsanid)) + messages.append("creating vsan " + str(vsanid)) + + if vsanname is not None: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, and cannot be renamed") + else: + if vsanname == sw_vsanname: + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which has the name " + + vsanname + + " Hence there is nothing to configure", + ) + else: + commands.append("vsan " + str(vsanid) + " name " + vsanname) + messages.append("setting vsan name to " + vsanname + " for vsan " + str(vsanid)) + + if vsansuspend: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, and cannot be suspended") + else: + if sw_vsanstate == "suspended": + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which is in suspended state ", + ) + else: + commands.append("vsan " + str(vsanid) + " suspend") + messages.append("suspending the vsan " + str(vsanid)) + else: + if sw_vsanstate == "active": + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which is in active state ", + ) + else: + commands.append("no vsan " + str(vsanid) + " suspend") + messages.append("no suspending the vsan " + str(vsanid)) + + if vsaninterface_list is not None: + for each_interface_name in vsaninterface_list: + # For fcip,port-channel,vfc-port-channel need to remove the + # extra space to compare + temp = re.sub(" +", "", each_interface_name) + if temp in sw_vsaninterfaces: + messages.append( + each_interface_name + + " is already present in the vsan " + + str(vsanid) + + " interface list", + ) + else: + commands.append("vsan " + str(vsanid) + " interface " + each_interface_name) + messages.append( + "adding interface " + each_interface_name + " to vsan " + str(vsanid), + ) + + if len(commands) != 0: + commands = ["terminal dont-ask"] + ["vsan database"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + commands_executed = cmds + + if commands_executed: + if module.check_mode: + module.exit_json( + changed=False, + commands=commands_executed, + msg="Check Mode: No cmds issued to the hosts", + ) + else: + result["changed"] = True + load_config(module, commands_executed) + + result["messages"] = messages + result["commands"] = commands_executed + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_domain.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_domain.py new file mode 100644 index 00000000..d1e3c39f --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_domain.py @@ -0,0 +1,215 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vtp_domain +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VTP domain configuration. +description: +- Manages VTP domain configuration. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- VTP feature must be active on the device to use this module. +- This module is used to manage only VTP domain names. +- VTP domain names are case-sensible. +- If it's never been configured before, VTP version is set to 1 by default. Otherwise, + it leaves the previous configured version untouched. Use M(cisco.nxos.nxos_vtp_version) + to change it. +- Use this in combination with M(cisco.nxos.nxos_vtp_password) and M(cisco.nxos.nxos_vtp_version) + to fully manage VTP operations. +options: + domain: + description: + - VTP domain name. + required: true + type: str +""" + +EXAMPLES = """ +# ENSURE VTP DOMAIN IS CONFIGURED +- cisco.nxos.nxos_vtp_domain: + domain: ntc + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"domain": "ntc"} +existing: + description: + - k/v pairs of existing vtp domain + returned: always + type: dict + sample: {"domain": "testing", "version": "2", "vtp_password": "password"} +end_state: + description: k/v pairs of vtp domain after module execution + returned: always + type: dict + sample: {"domain": "ntc", "version": "2", "vtp_password": "password"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp domain ntc"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) + + +def execute_show_command(command, module, output="json"): + cmds = [{"command": command, "output": output}] + body = run_commands(module, cmds) + return body + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_vtp_config(module): + command = "show vtp status" + body = execute_show_command(command, module, "text")[0] + vtp_parsed = {} + + if body: + version_regex = r".*VTP version running\s+:\s+(?P<version>\d).*" + domain_regex = r".*VTP Domain Name\s+:\s+(?P<domain>\S+).*" + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()["version"] + except AttributeError: + version = "" + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()["domain"] + except AttributeError: + domain = "" + + if domain and version: + vtp_parsed["domain"] = domain + vtp_parsed["version"] = version + vtp_parsed["vtp_password"] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = "show vtp password" + output = "json" + cap = get_capabilities(module)["device_info"]["network_os_model"] + if re.search(r"Nexus 6", cap): + output = "text" + + body = execute_show_command(command, module, output)[0] + + if output == "json": + password = body.get("passwd", "") + else: + password = "" + rp = r"VTP Password: (\S+)" + mo = re.search(rp, body) + if mo: + password = mo.group(1) + + return str(password) + + +def main(): + argument_spec = dict(domain=dict(type="str", required=True)) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + domain = module.params["domain"] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(domain=domain) + + changed = False + proposed = dict((k, v) for k, v in args.items() if v is not None) + delta = dict(set(proposed.items()).difference(existing.items())) + + commands = [] + if delta: + commands.append(["vtp domain {0}".format(domain)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_vtp_config(module) + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["end_state"] = end_state + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_password.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_password.py new file mode 100644 index 00000000..c6e9cabd --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_password.py @@ -0,0 +1,277 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vtp_password +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VTP password configuration. +description: +- Manages VTP password configuration. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- VTP feature must be active on the device to use this module. +- This module is used to manage only VTP passwords. +- Use this in combination with M(cisco.nxos.nxos_vtp_domain) and M(cisco.nxos.nxos_vtp_version) to fully + manage VTP operations. +- You can set/remove password only if a VTP domain already exist. +- If C(state=absent) and no C(vtp_password) is provided, it remove the current VTP + password. +- If C(state=absent) and C(vtp_password) is provided, the proposed C(vtp_password) + has to match the existing one in order to remove it. +options: + vtp_password: + description: + - VTP password + type: str + state: + description: + - Manage the state of the resource + default: present + choices: + - present + - absent + type: str +""" + +EXAMPLES = """ +# ENSURE VTP PASSWORD IS SET +- cisco.nxos.nxos_vtp_password: + state: present + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' + +# ENSURE VTP PASSWORD IS REMOVED +- cisco.nxos.nxos_vtp_password: + state: absent + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"vtp_password": "new_ntc"} +existing: + description: + - k/v pairs of existing vtp + returned: always + type: dict + sample: {"domain": "ntc", "version": "1", "vtp_password": "ntc"} +end_state: + description: k/v pairs of vtp after module execution + returned: always + type: dict + sample: {"domain": "ntc", "version": "1", "vtp_password": "new_ntc"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp password new_ntc"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) + + +def execute_show_command(command, module, output="json"): + cmds = [{"command": command, "output": output}] + body = run_commands(module, cmds) + return body + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = str(value) + else: + new_dict[new_key] = value + return new_dict + + +def get_vtp_config(module): + command = "show vtp status" + + body = execute_show_command(command, module, "text")[0] + vtp_parsed = {} + + if body: + version_regex = r".*VTP version running\s+:\s+(?P<version>\d).*" + domain_regex = r".*VTP Domain Name\s+:\s+(?P<domain>\S+).*" + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()["version"] + except AttributeError: + version = "" + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()["domain"] + except AttributeError: + domain = "" + + if domain and version: + vtp_parsed["domain"] = domain + vtp_parsed["version"] = version + vtp_parsed["vtp_password"] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = "show vtp password" + output = "json" + cap = get_capabilities(module)["device_info"]["network_os_model"] + if re.search(r"Nexus 6", cap): + output = "text" + + body = execute_show_command(command, module, output)[0] + + if output == "json": + password = body.get("passwd", "") + else: + password = "" + rp = r"VTP Password: (\S+)" + mo = re.search(rp, body) + if mo: + password = mo.group(1) + + return str(password) + + +def main(): + argument_spec = dict( + vtp_password=dict(type="str", no_log=True), + state=dict(choices=["absent", "present"], default="present"), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + vtp_password = module.params["vtp_password"] or None + state = module.params["state"] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(vtp_password=vtp_password) + + changed = False + proposed = dict((k, v) for k, v in args.items() if v is not None) + delta = dict(set(proposed.items()).difference(existing.items())) + + commands = [] + if state == "absent": + # if vtp_password is not set, some devices returns '\\' or the string 'None' + if ( + not existing["vtp_password"] + or existing["vtp_password"] == "\\" + or existing["vtp_password"] == "None" + ): + pass + elif vtp_password is not None: + if existing["vtp_password"] == proposed["vtp_password"]: + commands.append(["no vtp password"]) + else: + module.fail_json( + msg="Proposed vtp password doesn't match " + "current vtp password. It cannot be " + "removed when state=absent. If you are " + "trying to change the vtp password, use " + "state=present.", + ) + else: + if not existing.get("domain"): + module.fail_json(msg="Cannot remove a vtp password " "before vtp domain is set.") + + elif existing["vtp_password"] != ("\\"): + commands.append(["no vtp password"]) + + elif state == "present": + if delta: + if not existing.get("domain"): + module.fail_json(msg="Cannot set vtp password " "before vtp domain is set.") + + else: + commands.append(["vtp password {0}".format(vtp_password)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_vtp_config(module) + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["end_state"] = end_state + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_version.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_version.py new file mode 100644 index 00000000..8c5305a0 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vtp_version.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vtp_version +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VTP version configuration. +description: +- Manages VTP version configuration. +version_added: 1.0.0 +author: +- Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- VTP feature must be active on the device to use this module. +- This module is used to manage only VTP version. +- Use this in combination with M(cisco.nxos.nxos_vtp_password) and M(cisco.nxos.nxos_vtp_version) + to fully manage VTP operations. +options: + version: + description: + - VTP version number. + required: true + choices: + - '1' + - '2' + type: str +""" +EXAMPLES = """ +# ENSURE VTP VERSION IS 2 +- cisco.nxos.nxos_vtp_version: + version: 2 + host: '{{ inventory_hostname }}' + username: '{{ un }}' + password: '{{ pwd }}' +""" + +RETURN = """ +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"version": "2"} +existing: + description: + - k/v pairs of existing vtp + returned: always + type: dict + sample: {"domain": "testing", "version": "1", "vtp_password": "password"} +end_state: + description: k/v pairs of vtp after module execution + returned: always + type: dict + sample: {"domain": "testing", "version": "2", "vtp_password": "password"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp version 2"] +changed: + description: check to see if a change was made on the device + returned: always + type: bool + sample: true +""" +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_capabilities, + load_config, + run_commands, +) + + +def execute_show_command(command, module, output="json"): + cmds = [{"command": command, "output": output}] + body = run_commands(module, cmds) + return body + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def get_vtp_config(module): + command = "show vtp status" + body = execute_show_command(command, module, "text")[0] + vtp_parsed = {} + + if body: + version_regex = r".*VTP version running\s+:\s+(?P<version>\d).*" + domain_regex = r".*VTP Domain Name\s+:\s+(?P<domain>\S+).*" + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()["version"] + except AttributeError: + version = "" + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()["domain"] + except AttributeError: + domain = "" + + if domain and version: + vtp_parsed["domain"] = domain + vtp_parsed["version"] = version + vtp_parsed["vtp_password"] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = "show vtp password" + output = "json" + cap = get_capabilities(module)["device_info"]["network_os_model"] + if re.search(r"Nexus 6", cap): + output = "text" + + body = execute_show_command(command, module, output)[0] + + if output == "json": + password = body.get("passwd", "") + else: + password = "" + rp = r"VTP Password: (\S+)" + mo = re.search(rp, body) + if mo: + password = mo.group(1) + + return str(password) + + +def main(): + argument_spec = dict(version=dict(type="str", choices=["1", "2"], required=True)) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + + version = module.params["version"] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(version=version) + + changed = False + proposed = dict((k, v) for k, v in args.items() if v is not None) + delta = dict(set(proposed.items()).difference(existing.items())) + + commands = [] + if delta: + commands.append(["vtp version {0}".format(version)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + load_config(module, cmds) + end_state = get_vtp_config(module) + if "configure" in cmds: + cmds.pop(0) + + results = {} + results["proposed"] = proposed + results["existing"] = existing + results["end_state"] = end_state + results["updates"] = cmds + results["changed"] = changed + results["warnings"] = warnings + + module.exit_json(**results) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep.py new file mode 100644 index 00000000..ce311388 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep.py @@ -0,0 +1,458 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vxlan_vtep +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Manages VXLAN Network Virtualization Endpoint (NVE). +description: +- Manages VXLAN Network Virtualization Endpoint (NVE) overlay interface that terminates + VXLAN tunnels. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- The module is used to manage NVE properties, not to create NVE interfaces. Use M(cisco.nxos.nxos_interfaces) + if you wish to do so. +- C(state=absent) removes the interface. +- Default, where supported, restores params default value. +options: + interface: + description: + - Interface name for the VXLAN Network Virtualization Endpoint. + required: true + type: str + description: + description: + - Description of the NVE interface. + type: str + host_reachability: + description: + - Specify mechanism for host reachability advertisement. A Boolean value of 'true' + indicates that BGP will be used for host reachability advertisement. A Boolean + value of 'false' indicates that no protocol is used for host reachability advertisement. + Other host reachability advertisement protocols (e.g. OpenFlow, controller, etc.) are not + supported. + type: bool + shutdown: + description: + - Administratively shutdown the NVE interface. + type: bool + source_interface: + description: + - Specify the loopback interface whose IP address should be used for the NVE interface. + type: str + source_interface_hold_down_time: + description: + - Suppresses advertisement of the NVE loopback address until the overlay has converged. + type: str + global_mcast_group_L3: + description: + - Global multicast IP prefix for L3 VNIs or the keyword 'default'. This is available on + Nexus 9000 series switches running NX-OS software release 9.2(x) or higher. + type: str + global_mcast_group_L2: + description: + - Global multicast IP prefix for L2 VNIs or the keyword 'default'. This is available on + Nexus 9000 series switches running NX-OS software release 9.2(x) or higher. + type: str + global_suppress_arp: + description: + - Enables ARP suppression for all VNIs. This is available on NX-OS 9K series running + 9.2.x or higher. + type: bool + global_ingress_replication_bgp: + description: + - Configures ingress replication protocol as bgp for all VNIs. This is available on Nexus + 9000 series switches running NX-OS software release 9.2(x) or higher. + type: bool + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str + multisite_border_gateway_interface: + description: + - Specify the loopback interface whose IP address should be used for the NVE + Multisite Border-gateway Interface. This is available on specific Nexus 9000 + series switches running NX-OS 7.0(3)I7(x) or higher. Specify "default" to remove + an existing gateway config. + type: str + version_added: 1.1.0 +""" +EXAMPLES = """ +- cisco.nxos.nxos_vxlan_vtep: + interface: nve1 + description: default + host_reachability: true + source_interface: Loopback0 + source_interface_hold_down_time: 30 + shutdown: default + multisite_border_gateway_interface: Loopback0 +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface nve1", "source-interface loopback0", + "source-interface hold-down-time 30", "description simple description", + "shutdown", "host-reachability protocol bgp", + "multisite border-gateway interface loopback0"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, + run_commands, +) + + +BOOL_PARAMS = [ + "shutdown", + "host_reachability", + "global_ingress_replication_bgp", + "global_suppress_arp", +] +PARAM_TO_COMMAND_KEYMAP = { + "description": "description", + "global_suppress_arp": "global suppress-arp", + "global_ingress_replication_bgp": "global ingress-replication protocol bgp", + "global_mcast_group_L3": "global mcast-group L3", + "global_mcast_group_L2": "global mcast-group L2", + "host_reachability": "host-reachability protocol bgp", + "interface": "interface", + "shutdown": "shutdown", + "source_interface": "source-interface", + "source_interface_hold_down_time": "source-interface hold-down-time", + "multisite_border_gateway_interface": "multisite border-gateway interface", +} +PARAM_TO_DEFAULT_KEYMAP = { + "description": False, + "shutdown": True, + "source_interface_hold_down_time": "180", +} + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r"\s+{0}\s*$".format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + NO_SHUT_REGEX = re.compile(r"\s+no shutdown\s*$", re.M) + value = False + if arg == "shutdown": + try: + if NO_SHUT_REGEX.search(config): + value = False + elif REGEX.search(config): + value = True + except TypeError: + value = False + else: + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile( + r"(?:{0}\s)(?P<value>.*)$".format(PARAM_TO_COMMAND_KEYMAP[arg]), + re.M, + ) + NO_DESC_REGEX = re.compile(r"\s+{0}\s*$".format("no description"), re.M) + SOURCE_INTF_REGEX = re.compile( + r"(?:{0}\s)(?P<value>\S+)$".format(PARAM_TO_COMMAND_KEYMAP[arg]), + re.M, + ) + value = "" + if arg == "description": + if NO_DESC_REGEX.search(config): + value = False + elif PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group("value").strip() + elif arg == "source_interface": + for line in config.splitlines(): + try: + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = SOURCE_INTF_REGEX.search(config).group("value").strip() + break + except AttributeError: + value = "" + elif arg == "global_mcast_group_L2": + for line in config.splitlines(): + try: + if "global mcast-group" in line and "L2" in line: + value = line.split()[2].strip() + break + except AttributeError: + value = "" + elif arg == "global_mcast_group_L3": + for line in config.splitlines(): + try: + if "global mcast-group" in line and "L3" in line: + value = line.split()[2].strip() + break + except AttributeError: + value = "" + elif arg == "multisite_border_gateway_interface": + for line in config.splitlines(): + try: + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = SOURCE_INTF_REGEX.search(config).group("value").strip() + break + except AttributeError: + value = "" + else: + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group("value").strip() + return value + + +def get_existing(module, args): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module, flags=["all"])) + + interface_string = "interface {0}".format(module.params["interface"].lower()) + parents = [interface_string] + config = netcfg.get_section(parents) + + if config: + for arg in args: + existing[arg] = get_value(arg, config, module) + + existing["interface"] = module.params["interface"].lower() + else: + if interface_string in str(netcfg): + existing["interface"] = module.params["interface"].lower() + for arg in args: + existing[arg] = "" + return existing + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + value = table.get(key) + if value: + new_dict[new_key] = value + else: + new_dict[new_key] = value + return new_dict + + +def fix_commands(commands, module): + source_interface_command = "" + no_source_interface_command = "" + no_host_reachability_command = "" + host_reachability_command = "" + + for command in commands: + if "no source-interface hold-down-time" in command: + pass + elif "source-interface hold-down-time" in command: + pass + elif "no source-interface" in command: + no_source_interface_command = command + elif "source-interface" in command: + source_interface_command = command + elif "no host-reachability" in command: + no_host_reachability_command = command + elif "host-reachability" in command: + host_reachability_command = command + + if host_reachability_command: + commands.pop(commands.index(host_reachability_command)) + commands.insert(0, host_reachability_command) + + if source_interface_command: + commands.pop(commands.index(source_interface_command)) + commands.insert(0, source_interface_command) + + if no_host_reachability_command: + commands.pop(commands.index(no_host_reachability_command)) + commands.append(no_host_reachability_command) + + if no_source_interface_command: + commands.pop(commands.index(no_source_interface_command)) + commands.append(no_source_interface_command) + + commands.insert(0, "terminal dont-ask") + return commands + + +def gsa_tcam_check(module): + """ + global_suppress_arp is an N9k-only command that requires TCAM resources. + This method checks the current TCAM allocation. + Note that changing tcam_size requires a switch reboot to take effect. + """ + cmds = [{"command": "show hardware access-list tcam region", "output": "json"}] + body = run_commands(module, cmds) + if body: + tcam_region = body[0]["TCAM_Region"]["TABLE_Sizes"]["ROW_Sizes"] + if bool( + [ + i + for i in tcam_region + if i["type"].startswith("Ingress ARP-Ether ACL") and i["tcam_size"] == "0" + ], + ): + msg = ( + "'show hardware access-list tcam region' indicates 'ARP-Ether' tcam size is 0 (no allocated resources). " + + "'global_suppress_arp' will be rejected by device." + ) + module.fail_json(msg=msg) + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.items(): + if value is True: + commands.append(key) + + elif value is False: + commands.append("no {0}".format(key)) + + elif value == "default": + if existing_commands.get(key): + existing_value = existing_commands.get(key) + if "global mcast-group" in key: + commands.append("no {0}".format(key)) + else: + commands.append("no {0} {1}".format(key, existing_value)) + else: + if key.replace(" ", "_").replace("-", "_") in BOOL_PARAMS: + commands.append("no {0}".format(key.lower())) + module.exit_json(commands=commands) + else: + if "L2" in key: + commands.append("global mcast-group " + value + " L2") + elif "L3" in key: + commands.append("global mcast-group " + value + " L3") + else: + command = "{0} {1}".format(key, value.lower()) + commands.append(command) + + if commands: + commands = fix_commands(commands, module) + parents = ["interface {0}".format(module.params["interface"].lower())] + candidate.add(commands, parents=parents) + else: + if not existing and module.params["interface"]: + commands = ["interface {0}".format(module.params["interface"].lower())] + candidate.add(commands, parents=[]) + + +def state_absent(module, existing, proposed, candidate): + commands = ["no interface {0}".format(module.params["interface"].lower())] + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + interface=dict(required=True, type="str"), + description=dict(required=False, type="str"), + host_reachability=dict(required=False, type="bool"), + global_ingress_replication_bgp=dict(required=False, type="bool"), + global_suppress_arp=dict(required=False, type="bool"), + global_mcast_group_L2=dict(required=False, type="str"), + global_mcast_group_L3=dict(required=False, type="str"), + shutdown=dict(required=False, type="bool"), + source_interface=dict(required=False, type="str"), + source_interface_hold_down_time=dict(required=False, type="str"), + state=dict(choices=["present", "absent"], default="present", required=False), + multisite_border_gateway_interface=dict(required=False, type="str"), + ) + + mutually_exclusive = [("global_ingress_replication_bgp", "global_mcast_group_L2")] + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + result = {"changed": False, "commands": [], "warnings": warnings} + + state = module.params["state"] + + args = PARAM_TO_COMMAND_KEYMAP.keys() + + existing = get_existing(module, args) + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + proposed = {} + for key, value in proposed_args.items(): + if key != "interface": + if str(value).lower() == "default": + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + if key in BOOL_PARAMS: + value = False + else: + value = "default" + if str(existing.get(key)).lower() != str(value).lower(): + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + + if proposed.get("global_suppress_arp"): + gsa_tcam_check(module) + if state == "present": + if not existing: + warnings.append( + "The proposed NVE interface did not exist. " + "It's recommended to use nxos_interfaces to create " + "all logical interfaces.", + ) + state_present(module, existing, proposed, candidate) + elif state == "absent" and existing: + state_absent(module, existing, proposed, candidate) + + if candidate: + candidate = candidate.items_text() + result["commands"] = candidate + result["changed"] = True + load_config(module, candidate) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep_vni.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep_vni.py new file mode 100644 index 00000000..d58bd6c9 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_vxlan_vtep_vni.py @@ -0,0 +1,452 @@ +#!/usr/bin/python +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# +from __future__ import absolute_import, division, print_function + + +__metaclass__ = type + + +DOCUMENTATION = """ +module: nxos_vxlan_vtep_vni +extends_documentation_fragment: +- cisco.nxos.nxos +short_description: Creates a Virtual Network Identifier member (VNI) +description: +- Creates a Virtual Network Identifier member (VNI) for an NVE overlay interface. +version_added: 1.0.0 +author: Gabriele Gerbino (@GGabriele) +notes: +- Tested against NXOSv 7.3.(0)D1(1) on VIRL +- Unsupported for Cisco MDS +- default, where supported, restores params default value. +options: + interface: + description: + - Interface name for the VXLAN Network Virtualization Endpoint. + required: true + type: str + vni: + description: + - ID of the Virtual Network Identifier. + required: true + type: str + assoc_vrf: + description: + - This attribute is used to identify and separate processing VNIs that are associated + with a VRF and used for routing. The VRF and VNI specified with this command + must match the configuration of the VNI under the VRF. + type: bool + ingress_replication: + description: + - Specifies mechanism for host reachability advertisement. + choices: + - bgp + - static + - default + type: str + multicast_group: + description: + - The multicast group (range) of the VNI. Valid values are string and keyword + 'default'. + type: str + peer_list: + description: + - Set the ingress-replication static peer list. Valid values are an array, a space-separated + string of ip addresses, or the keyword 'default'. + type: list + elements: str + suppress_arp: + description: + - Suppress arp under layer 2 VNI. + type: bool + suppress_arp_disable: + description: + - Overrides the global ARP suppression config. This is available on NX-OS 9K series + running 9.2.x or higher. + type: bool + state: + description: + - Determines whether the config should be present or not on the device. + default: present + choices: + - present + - absent + type: str + multisite_ingress_replication: + description: + - Enables multisite ingress replication. + choices: + - disable + - enable + - optimized + type: str + version_added: 1.1.0 +""" +EXAMPLES = """ +- cisco.nxos.nxos_vxlan_vtep_vni: + interface: nve1 + vni: 6000 + ingress_replication: default + multisite_ingress_replication: enable +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: ["interface nve1", "member vni 6000", "multisite ingress-replication"] +""" + +import re + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import ( + CustomNetworkConfig, +) + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + get_config, + load_config, +) + + +BOOL_PARAMS = ["assoc_vrf", "suppress_arp", "suppress_arp_disable"] +PARAM_TO_DEFAULT_KEYMAP = { + "multicast_group": "", + "peer_list": [], + "ingress_replication": "", +} +PARAM_TO_COMMAND_KEYMAP = { + "assoc_vrf": "associate-vrf", + "interface": "interface", + "vni": "member vni", + "ingress_replication": "ingress-replication protocol", + "multicast_group": "mcast-group", + "peer_list": "peer-ip", + "suppress_arp": "suppress-arp", + "suppress_arp_disable": "suppress-arp disable", + "multisite_ingress_replication": "multisite ingress-replication", +} + + +def get_value(arg, config, module): + command = PARAM_TO_COMMAND_KEYMAP[arg] + command_val_re = re.compile(r"(?:{0}\s)(?P<value>.*)$".format(command), re.M) + + if arg in BOOL_PARAMS: + command_re = re.compile(r"\s+{0}\s*$".format(command), re.M) + value = False + if command_re.search(config): + value = True + elif arg == "peer_list": + has_command_val = command_val_re.findall(config, re.M) + value = [] + if has_command_val: + value = has_command_val + elif arg == "multisite_ingress_replication": + has_command = re.search(r"^\s+{0}$".format(command), config, re.M) + has_command_val = command_val_re.search(config, re.M) + value = "disable" + if has_command: + value = "enable" + elif has_command_val: + value = "optimized" + else: + value = "" + has_command_val = command_val_re.search(config, re.M) + if has_command_val: + value = has_command_val.group("value") + return value + + +def check_interface(module, netcfg): + config = str(netcfg) + + has_interface = re.search(r"(?:interface nve)(?P<value>.*)$", config, re.M) + value = "" + if has_interface: + value = "nve{0}".format(has_interface.group("value")) + + return value + + +def get_existing(module, args): + existing = {} + netcfg = CustomNetworkConfig(indent=2, contents=get_config(module)) + + interface_exist = check_interface(module, netcfg) + if interface_exist: + parents = ["interface {0}".format(interface_exist)] + temp_config = netcfg.get_section(parents) + + if "member vni {0} associate-vrf".format(module.params["vni"]) in temp_config: + parents.append("member vni {0} associate-vrf".format(module.params["vni"])) + config = netcfg.get_section(parents) + elif "member vni {0}".format(module.params["vni"]) in temp_config: + parents.append("member vni {0}".format(module.params["vni"])) + config = netcfg.get_section(parents) + else: + config = {} + + if config: + for arg in args: + if arg not in ["interface", "vni"]: + existing[arg] = get_value(arg, config, module) + existing["interface"] = interface_exist + existing["vni"] = module.params["vni"] + + return existing, interface_exist + + +def apply_key_map(key_map, table): + new_dict = {} + for key, value in table.items(): + new_key = key_map.get(key) + if new_key: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.items(): + if key == "associate-vrf": + command = "member vni {0} {1}".format(module.params["vni"], key) + if not value: + command = "no {0}".format(command) + commands.append(command) + + elif key == "peer-ip" and value != []: + for peer in value: + commands.append("{0} {1}".format(key, peer)) + + elif key == "mcast-group" and value != existing_commands.get(key): + commands.append("no {0}".format(key)) + vni_command = "member vni {0}".format(module.params["vni"]) + if vni_command not in commands: + commands.append("member vni {0}".format(module.params["vni"])) + if value != PARAM_TO_DEFAULT_KEYMAP.get("multicast_group", "default"): + commands.append("{0} {1}".format(key, value)) + + elif key == "ingress-replication protocol" and value != existing_commands.get(key): + evalue = existing_commands.get(key) + dvalue = PARAM_TO_DEFAULT_KEYMAP.get("ingress_replication", "default") + if value != dvalue: + if evalue and evalue != dvalue: + commands.append("no {0} {1}".format(key, evalue)) + commands.append("{0} {1}".format(key, value)) + else: + if evalue: + commands.append("no {0} {1}".format(key, evalue)) + + elif value is True: + commands.append(key) + elif value is False: + commands.append("no {0}".format(key)) + elif value == "default" or value == []: + if existing_commands.get(key): + existing_value = existing_commands.get(key) + if key == "peer-ip": + for peer in existing_value: + commands.append("no {0} {1}".format(key, peer)) + else: + commands.append("no {0} {1}".format(key, existing_value)) + else: + if key.replace(" ", "_").replace("-", "_") in BOOL_PARAMS: + commands.append("no {0}".format(key.lower())) + elif key == "multisite ingress-replication" and value != existing_commands.get(key): + vni_command = "member vni {0}".format(module.params["vni"]) + if vni_command not in commands: + commands.append("member vni {0}".format(module.params["vni"])) + if value == "disable": + command = "no {0}".format(key) + commands.append(command) + elif value == "enable": + command = "{0}".format(key) + commands.append(command) + elif value == "optimized": + command = "{0} {1}".format(key, value) + commands.append(command) + else: + command = "{0} {1}".format(key, value.lower()) + commands.append(command) + + if commands: + vni_command = "member vni {0}".format(module.params["vni"]) + ingress_replications_command = "ingress-replication protocol static" + ingress_replicationb_command = "ingress-replication protocol bgp" + ingress_replicationns_command = "no ingress-replication protocol static" + ingress_replicationnb_command = "no ingress-replication protocol bgp" + interface_command = "interface {0}".format(module.params["interface"]) + + if any( + c in commands + for c in ( + ingress_replications_command, + ingress_replicationb_command, + ingress_replicationnb_command, + ingress_replicationns_command, + ) + ): + static_level_cmds = [cmd for cmd in commands if "peer" in cmd] + parents = [interface_command, vni_command] + commands = [cmd for cmd in commands if "peer" not in cmd] + for cmd in commands: + parents.append(cmd) + candidate.add(static_level_cmds, parents=parents) + + elif "peer-ip" in commands[0]: + static_level_cmds = list(commands) + parents = [ + interface_command, + vni_command, + ingress_replications_command, + ] + candidate.add(static_level_cmds, parents=parents) + + if vni_command in commands: + parents = [interface_command] + commands.remove(vni_command) + if module.params["assoc_vrf"] is None: + parents.append(vni_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + if existing["assoc_vrf"]: + commands = ["no member vni {0} associate-vrf".format(module.params["vni"])] + else: + commands = ["no member vni {0}".format(module.params["vni"])] + parents = ["interface {0}".format(module.params["interface"])] + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + interface=dict(required=True, type="str"), + vni=dict(required=True, type="str"), + assoc_vrf=dict(required=False, type="bool"), + multicast_group=dict(required=False, type="str"), + peer_list=dict(required=False, type="list", elements="str"), + suppress_arp=dict(required=False, type="bool"), + suppress_arp_disable=dict(required=False, type="bool"), + ingress_replication=dict(required=False, type="str", choices=["bgp", "static", "default"]), + state=dict(choices=["present", "absent"], default="present", required=False), + multisite_ingress_replication=dict( + required=False, + type="str", + choices=["enable", "optimized", "disable"], + ), + ) + + mutually_exclusive = [ + ("suppress_arp", "suppress_arp_disable"), + ("assoc_vrf", "multicast_group"), + ("assoc_vrf", "suppress_arp"), + ("assoc_vrf", "suppress_arp_disable"), + ("assoc_vrf", "ingress_replication"), + ] + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) + + warnings = list() + result = {"changed": False, "commands": [], "warnings": warnings} + + if module.params["peer_list"]: + if ( + module.params["peer_list"][0] != "default" + and module.params["ingress_replication"] != "static" + ): + module.fail_json( + msg="ingress_replication=static is required " "when using peer_list param", + ) + else: + peer_list = module.params["peer_list"] + if peer_list[0] == "default": + module.params["peer_list"] = "default" + else: + stripped_peer_list = list(map(str.strip, peer_list)) + module.params["peer_list"] = stripped_peer_list + + if ( + module.params["multisite_ingress_replication"] == "enable" + or module.params["multisite_ingress_replication"] == "optimized" + ): + if module.params["ingress_replication"] == "static": + module.fail_json( + msg="ingress_replication=static is not allowed " + "when using multisite_ingress_replication", + ) + + state = module.params["state"] + args = PARAM_TO_COMMAND_KEYMAP.keys() + existing, interface_exist = get_existing(module, args) + + if state == "present": + if not interface_exist: + module.fail_json( + msg="The proposed NVE interface does not exist. Use nxos_interface to create it first.", + ) + elif interface_exist != module.params["interface"]: + module.fail_json(msg="Only 1 NVE interface is allowed on the switch.") + elif state == "absent": + if interface_exist != module.params["interface"]: + module.exit_json(**result) + elif existing and existing["vni"] != module.params["vni"]: + module.fail_json( + msg="ERROR: VNI delete failed: Could not find vni node for {0}".format( + module.params["vni"], + ), + existing_vni=existing["vni"], + ) + + proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.items(): + if key in ["multicast_group", "peer_list", "ingress_replication"]: + if str(value).lower() == "default": + value = PARAM_TO_DEFAULT_KEYMAP.get(key, "default") + if key != "interface" and existing.get(key) != value: + proposed[key] = value + + candidate = CustomNetworkConfig(indent=3) + if state == "present": + state_present(module, existing, proposed, candidate) + elif existing and state == "absent": + state_absent(module, existing, proposed, candidate) + + if candidate: + candidate = candidate.items_text() + result["changed"] = True + result["commands"] = candidate + if not module.check_mode: + load_config(module, candidate) + + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/nxos_zone_zoneset.py b/ansible_collections/cisco/nxos/plugins/modules/nxos_zone_zoneset.py new file mode 100644 index 00000000..7c9fba30 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/nxos_zone_zoneset.py @@ -0,0 +1,888 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_zone_zoneset +short_description: Configuration of zone/zoneset for Cisco NXOS MDS Switches. +description: +- Configuration of zone/zoneset for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + zone_zoneset_details: + description: + - List of zone/zoneset details to be added or removed + type: list + elements: dict + suboptions: + vsan: + description: + - vsan id + required: true + type: int + mode: + description: + - mode of the zone for the vsan + choices: + - enhanced + - basic + type: str + default_zone: + description: + - default zone behaviour for the vsan + choices: + - permit + - deny + type: str + smart_zoning: + description: + - Removes the vsan if True + type: bool + zone: + description: + - List of zone options for that vsan + type: list + elements: dict + suboptions: + name: + description: + - name of the zone + required: true + type: str + remove: + description: + - Deletes the zone if True + type: bool + default: false + members: + description: + - Members of the zone that needs to be removed or added + type: list + elements: dict + suboptions: + pwwn: + description: + - pwwn member of the zone, use alias 'device_alias' as option for + device_alias member + aliases: + - device_alias + required: true + type: str + remove: + description: + - Removes member from the zone if True + type: bool + default: false + devtype: + description: + - devtype of the zone member used along with Smart zoning config + choices: + - initiator + - target + - both + type: str + zoneset: + description: + - List of zoneset options for the vsan + type: list + elements: dict + suboptions: + name: + description: + - name of the zoneset + required: true + type: str + remove: + description: + - Removes zoneset if True + type: bool + default: false + action: + description: + - activates/de-activates the zoneset + choices: + - activate + - deactivate + type: str + members: + description: + - Members of the zoneset that needs to be removed or added + type: list + elements: dict + suboptions: + name: + description: + - name of the zone that needs to be added to the zoneset or removed + from the zoneset + required: true + type: str + remove: + description: + - Removes zone member from the zoneset + type: bool + default: false +""" + +EXAMPLES = """ +- name: Test that zone/zoneset module works + cisco.nxos.nxos_zone_zoneset: + zone_zoneset_details: + - mode: enhanced + vsan: 22 + zone: + - members: + - pwwn: 11:11:11:11:11:11:11:11 + - device_alias: test123 + - pwwn: 61:61:62:62:12:12:12:12 + remove: true + name: zoneA + - members: + - pwwn: 10:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:21:21:21:21 + name: zoneB + - name: zoneC + remove: true + zoneset: + - action: activate + members: + - name: zoneA + - name: zoneB + - name: zoneC + remove: true + name: zsetname1 + - action: deactivate + name: zsetTestExtra + remove: true + - mode: basic + smart_zoning: true + vsan: 21 + zone: + - members: + - devtype: both + pwwn: 11:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:12:12:12:12 + - devtype: both + pwwn: 92:62:62:62:12:12:1a:1a + remove: true + name: zone21A + - members: + - pwwn: 10:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:21:21:21:21 + name: zone21B + zoneset: + - action: activate + members: + - name: zone21A + - name: zone21B + name: zsetname212 + +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - zone name zoneA vsan 923 + - member pwwn 11:11:11:11:11:11:11:11 + - no member device-alias test123 + - zone commit vsan 923 + - no terminal dont-ask +messages: + description: debug messages + returned: always + type: list + sample: + - "zone mode is already enhanced ,no change in zone mode configuration for vsan 922" + - "zone member '11:11:11:11:11:11:11:11' is already present in zone 'zoneA' in vsan 922 hence nothing to add" + - "zone member 'test123' is already present in zone 'zoneA' in vsan 922 hence nothing to add" + - "zone member '61:61:62:62:12:12:12:12' is not present in zone 'zoneA' in vsan 922 hence nothing to remove" + - "zone member '10:11:11:11:11:11:11:11' is already present in zone 'zoneB' in vsan 922 hence nothing to add" + - "zone member '62:62:62:62:21:21:21:21' is already present in zone 'zoneB' in vsan 922 hence nothing to add" + - "zone 'zoneC' is not present in vsan 922 , so nothing to remove" +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + + +class ShowZonesetActive(object): + """docstring for ShowZonesetActive""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.activeZSName = None + self.parseCmdOutput() + + def execute_show_zoneset_active_cmd(self): + command = "show zoneset active vsan " + str(self.vsan) + " | grep zoneset" + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZoneset = r"zoneset name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zoneset_active_cmd().split("\n") + if len(output) == 0: + return + else: + for line in output: + line = line.strip() + mzs = re.match(patZoneset, line.strip()) + if mzs: + self.activeZSName = mzs.group(1).strip() + return + + def isZonesetActive(self, zsname): + if zsname == self.activeZSName: + return True + return False + + +class ShowZoneset(object): + """docstring for ShowZoneset""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.zsDetails = {} + self.parseCmdOutput() + + def execute_show_zoneset_cmd(self): + command = "show zoneset vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZoneset = r"zoneset name (\S+) vsan " + str(self.vsan) + patZone = r"zone name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zoneset_cmd().split("\n") + for line in output: + line = line.strip() + mzs = re.match(patZoneset, line.strip()) + mz = re.match(patZone, line.strip()) + if mzs: + zonesetname = mzs.group(1).strip() + self.zsDetails[zonesetname] = [] + continue + elif mz: + zonename = mz.group(1).strip() + v = self.zsDetails[zonesetname] + v.append(zonename) + self.zsDetails[zonesetname] = v + + def isZonesetPresent(self, zsname): + return zsname in self.zsDetails.keys() + + def isZonePresentInZoneset(self, zsname, zname): + if zsname in self.zsDetails.keys(): + return zname in self.zsDetails[zsname] + return False + + +class ShowZone(object): + """docstring for ShowZone""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.zDetails = {} + self.parseCmdOutput() + + def execute_show_zone_vsan_cmd(self): + command = "show zone vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZone = r"zone name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zone_vsan_cmd().split("\n") + for line in output: + line = re.sub(r"[\[].*?[\]]", "", line) + line = " ".join(line.strip().split()) + if "init" in line: + line = line.replace("init", "initiator") + m = re.match(patZone, line) + if m: + zonename = m.group(1).strip() + self.zDetails[zonename] = [] + continue + else: + # For now we support only pwwn and device-alias under zone + # Ideally should use 'supported_choices'....but maybe next + # time. + if "pwwn" in line or "device-alias" in line: + v = self.zDetails[zonename] + v.append(line) + self.zDetails[zonename] = v + + def isZonePresent(self, zname): + return zname in self.zDetails.keys() + + def isZoneMemberPresent(self, zname, cmd): + if zname in self.zDetails.keys(): + zonememlist = self.zDetails[zname] + for eachline in zonememlist: + if cmd in eachline: + return True + return False + + def get_zDetails(self): + return self.zDetails + + +class ShowZoneStatus(object): + """docstring for ShowZoneStatus""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.vsanAbsent = False + self.module = module + self.default_zone = "" + self.mode = "" + self.session = "" + self.sz = "" + self.locked = False + self.update() + + def execute_show_zone_status_cmd(self): + command = "show zone status vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def update(self): + output = self.execute_show_zone_status_cmd().split("\n") + + patfordefzone = "VSAN: " + str(self.vsan) + r" default-zone:\s+(\S+).*" + patformode = r".*mode:\s+(\S+).*" + patforsession = r"^session:\s+(\S+).*" + patforsz = r".*smart-zoning:\s+(\S+).*" + for line in output: + if "is not configured" in line: + self.vsanAbsent = True + break + mdefz = re.match(patfordefzone, line.strip()) + mmode = re.match(patformode, line.strip()) + msession = re.match(patforsession, line.strip()) + msz = re.match(patforsz, line.strip()) + + if mdefz: + self.default_zone = mdefz.group(1) + if mmode: + self.mode = mmode.group(1) + if msession: + self.session = msession.group(1) + if self.session != "none": + self.locked = True + if msz: + self.sz = msz.group(1) + + def isLocked(self): + return self.locked + + def getDefaultZone(self): + return self.default_zone + + def getMode(self): + return self.mode + + def getSmartZoningStatus(self): + return self.sz + + def isVsanAbsent(self): + return self.vsanAbsent + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + return run_commands(module, commands) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def getMemType(supported_choices, allmemkeys, default="pwwn"): + for eachchoice in supported_choices: + if eachchoice in allmemkeys: + return eachchoice + return default + + +def main(): + supported_choices = ["device_alias"] + zone_member_spec = dict( + pwwn=dict(required=True, type="str", aliases=["device_alias"]), + devtype=dict(type="str", choices=["initiator", "target", "both"]), + remove=dict(type="bool", default=False), + ) + + zone_spec = dict( + name=dict(required=True, type="str"), + members=dict(type="list", elements="dict", options=zone_member_spec), + remove=dict(type="bool", default=False), + ) + + zoneset_member_spec = dict( + name=dict(required=True, type="str"), + remove=dict(type="bool", default=False), + ) + + zoneset_spec = dict( + name=dict(type="str", required=True), + members=dict(type="list", elements="dict", options=zoneset_member_spec), + remove=dict(type="bool", default=False), + action=dict(type="str", choices=["activate", "deactivate"]), + ) + + zonedetails_spec = dict( + vsan=dict(required=True, type="int"), + mode=dict(type="str", choices=["enhanced", "basic"]), + default_zone=dict(type="str", choices=["permit", "deny"]), + smart_zoning=dict(type="bool"), + zone=dict(type="list", elements="dict", options=zone_spec), + zoneset=dict(type="list", elements="dict", options=zoneset_spec), + ) + + argument_spec = dict( + zone_zoneset_details=dict(type="list", elements="dict", options=zonedetails_spec), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + messages = list() + commands = list() + result = {"changed": False} + + commands_executed = [] + listOfZoneDetails = module.params["zone_zoneset_details"] + for eachZoneZonesetDetail in listOfZoneDetails: + vsan = eachZoneZonesetDetail["vsan"] + op_mode = eachZoneZonesetDetail["mode"] + op_default_zone = eachZoneZonesetDetail["default_zone"] + op_smart_zoning = eachZoneZonesetDetail["smart_zoning"] + op_zone = eachZoneZonesetDetail["zone"] + op_zoneset = eachZoneZonesetDetail["zoneset"] + + # Step1: execute show zone status and get + shZoneStatusObj = ShowZoneStatus(module, vsan) + sw_default_zone = shZoneStatusObj.getDefaultZone() + sw_mode = shZoneStatusObj.getMode() + sw_smart_zoning = shZoneStatusObj.getSmartZoningStatus() + + if sw_smart_zoning.lower() == "Enabled".lower(): + sw_smart_zoning_bool = True + else: + sw_smart_zoning_bool = False + + if shZoneStatusObj.isVsanAbsent(): + module.fail_json( + msg="Vsan " + str(vsan) + " is not present in the switch. Hence cannot procced.", + ) + + if shZoneStatusObj.isLocked(): + module.fail_json( + msg="zone has acquired lock on the switch for vsan " + + str(vsan) + + ". Hence cannot procced.", + ) + + # Process zone default zone options + if op_default_zone is not None: + if op_default_zone != sw_default_zone: + if op_default_zone == "permit": + commands_executed.append("zone default-zone permit vsan " + str(vsan)) + messages.append( + "default zone configuration changed from deny to permit for vsan " + + str(vsan), + ) + else: + commands_executed.append("no zone default-zone permit vsan " + str(vsan)) + messages.append( + "default zone configuration changed from permit to deny for vsan " + + str(vsan), + ) + else: + messages.append( + "default zone is already " + + op_default_zone + + " ,no change in default zone configuration for vsan " + + str(vsan), + ) + + # Process zone mode options + if op_mode is not None: + if op_mode != sw_mode: + if op_mode == "enhanced": + commands_executed.append("zone mode enhanced vsan " + str(vsan)) + messages.append( + "zone mode configuration changed from basic to enhanced for vsan " + + str(vsan), + ) + else: + commands_executed.append("no zone mode enhanced vsan " + str(vsan)) + messages.append( + "zone mode configuration changed from enhanced to basic for vsan " + + str(vsan), + ) + else: + messages.append( + "zone mode is already " + + op_mode + + " ,no change in zone mode configuration for vsan " + + str(vsan), + ) + + # Process zone smart-zone options + if op_smart_zoning is not None: + if op_smart_zoning != sw_smart_zoning_bool: + if op_smart_zoning: + commands_executed.append("zone smart-zoning enable vsan " + str(vsan)) + messages.append("smart-zoning enabled for vsan " + str(vsan)) + else: + commands_executed.append("no zone smart-zoning enable vsan " + str(vsan)) + messages.append("smart-zoning disabled for vsan " + str(vsan)) + else: + messages.append( + "smart-zoning is already set to " + + sw_smart_zoning + + " , no change in smart-zoning configuration for vsan " + + str(vsan), + ) + + # Process zone member options + # TODO: Obviously this needs to be cleaned up properly, as there are a lot of ifelse statements which is bad + # Will take it up later becoz of time constraints + if op_zone is not None: + shZoneObj = ShowZone(module, vsan) + for eachzone in op_zone: + zname = eachzone["name"] + zmembers = eachzone["members"] + removeflag = eachzone["remove"] + if removeflag: + if shZoneObj.isZonePresent(zname): + messages.append("zone '" + zname + "' is removed from vsan " + str(vsan)) + commands_executed.append("no zone name " + zname + " vsan " + str(vsan)) + else: + messages.append( + "zone '" + + zname + + "' is not present in vsan " + + str(vsan) + + " , so nothing to remove", + ) + else: + if zmembers is None: + if shZoneObj.isZonePresent(zname): + messages.append( + "zone '" + zname + "' is already present in vsan " + str(vsan), + ) + else: + commands_executed.append("zone name " + zname + " vsan " + str(vsan)) + messages.append("zone '" + zname + "' is created in vsan " + str(vsan)) + else: + cmdmemlist = [] + for eachmem in zmembers: + memtype = getMemType(supported_choices, eachmem.keys()) + cmd = memtype.replace("_", "-") + " " + eachmem[memtype] + if op_smart_zoning or sw_smart_zoning_bool: + if eachmem["devtype"] is not None: + cmd = cmd + " " + eachmem["devtype"] + if eachmem["remove"]: + if shZoneObj.isZonePresent(zname): + if shZoneObj.isZoneMemberPresent(zname, cmd): + cmd = "no member " + cmd + cmdmemlist.append(cmd) + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "removing zone member '" + + eachmem[memtype] + + " of device type '" + + eachmem["devtype"] + + "' from zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "removing zone member '" + + eachmem[memtype] + + "' from zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' is not present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to remove", + ) + else: + messages.append( + "zone member '" + + eachmem[memtype] + + "' is not present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to remove", + ) + else: + messages.append( + "zone '" + + zname + + "' is not present in vsan " + + str(vsan) + + " , hence cannot remove the members", + ) + + else: + if shZoneObj.isZoneMemberPresent(zname, cmd): + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' is already present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to add", + ) + else: + messages.append( + "zone member '" + + eachmem[memtype] + + "' is already present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to add", + ) + else: + cmd = "member " + cmd + cmdmemlist.append(cmd) + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "adding zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' to zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "adding zone member '" + + eachmem[memtype] + + "' to zone '" + + zname + + "' in vsan " + + str(vsan), + ) + if len(cmdmemlist) != 0: + commands_executed.append("zone name " + zname + " vsan " + str(vsan)) + commands_executed = commands_executed + cmdmemlist + + # Process zoneset member options + if op_zoneset is not None: + dactcmd = [] + actcmd = [] + shZonesetObj = ShowZoneset(module, vsan) + shZonesetActiveObj = ShowZonesetActive(module, vsan) + for eachzoneset in op_zoneset: + zsetname = eachzoneset["name"] + zsetmembers = eachzoneset["members"] + removeflag = eachzoneset["remove"] + actionflag = eachzoneset["action"] + if removeflag: + if shZonesetObj.isZonesetPresent(zsetname): + messages.append( + "zoneset '" + zsetname + "' is removed from vsan " + str(vsan), + ) + commands_executed.append( + "no zoneset name " + zsetname + " vsan " + str(vsan), + ) + else: + messages.append( + "zoneset '" + + zsetname + + "' is not present in vsan " + + str(vsan) + + " ,hence there is nothing to remove", + ) + else: + if zsetmembers is not None: + cmdmemlist = [] + for eachzsmem in zsetmembers: + zsetmem_name = eachzsmem["name"] + zsetmem_removeflag = eachzsmem["remove"] + if zsetmem_removeflag: + if shZonesetObj.isZonePresentInZoneset(zsetname, zsetmem_name): + cmd = "no member " + zsetmem_name + cmdmemlist.append(cmd) + messages.append( + "removing zoneset member '" + + zsetmem_name + + "' from zoneset '" + + zsetname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "zoneset member '" + + zsetmem_name + + "' is not present in zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " ,hence there is nothing to remove", + ) + else: + if shZonesetObj.isZonePresentInZoneset(zsetname, zsetmem_name): + messages.append( + "zoneset member '" + + zsetmem_name + + "' is already present in zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " ,hence there is nothing to add", + ) + else: + cmd = "member " + zsetmem_name + cmdmemlist.append(cmd) + messages.append( + "adding zoneset member '" + + zsetmem_name + + "' to zoneset '" + + zsetname + + "' in vsan " + + str(vsan), + ) + if len(cmdmemlist) != 0: + commands_executed.append( + "zoneset name " + zsetname + " vsan " + str(vsan), + ) + commands_executed = commands_executed + cmdmemlist + else: + if shZonesetObj.isZonesetPresent(zsetname): + messages.append( + "zoneset '" + + zsetname + + "' is already present in vsan " + + str(vsan), + ) + else: + commands_executed.append( + "zoneset name " + zsetname + " vsan " + str(vsan), + ) + messages.append( + "zoneset '" + zsetname + "' is created in vsan " + str(vsan), + ) + + # Process zoneset activate options + if actionflag == "deactivate": + if shZonesetActiveObj.isZonesetActive(zsetname): + messages.append( + "deactivating zoneset '" + zsetname + "' in vsan " + str(vsan), + ) + dactcmd.append( + "no zoneset activate name " + zsetname + " vsan " + str(vsan), + ) + else: + messages.append( + "zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " is not activated, hence cannot deactivate", + ) + elif actionflag == "activate": + if commands_executed: + messages.append( + "activating zoneset '" + zsetname + "' in vsan " + str(vsan), + ) + actcmd.append("zoneset activate name " + zsetname + " vsan " + str(vsan)) + else: + messages.append( + "no changes to existing zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " hence activate action is ignored", + ) + commands_executed = commands_executed + dactcmd + actcmd + + if commands_executed: + if op_mode == "enhanced": + commands_executed.append("zone commit vsan " + str(vsan)) + elif op_mode is None: + if sw_mode == "enhanced": + commands_executed.append("zone commit vsan " + str(vsan)) + + if commands_executed: + commands_executed = ["terminal dont-ask"] + commands_executed + ["no terminal dont-ask"] + + cmds = flatten_list(commands_executed) + if cmds: + if module.check_mode: + module.exit_json( + changed=False, + commands=cmds, + msg="Check Mode: No cmds issued to the hosts", + ) + else: + result["changed"] = True + commands = commands + cmds + load_config(module, cmds) + + result["messages"] = messages + result["commands"] = commands_executed + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/storage/__init__.py b/ansible_collections/cisco/nxos/plugins/modules/storage/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/storage/__init__.py diff --git a/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_devicealias.py b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_devicealias.py new file mode 100644 index 00000000..71d4ebb6 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_devicealias.py @@ -0,0 +1,550 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_devicealias +short_description: Configuration of device alias for Cisco NXOS MDS Switches. +description: +- Configuration of device alias for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + distribute: + description: + - Enable/Disable device-alias distribution + type: bool + mode: + description: + - Mode of devices-alias, basic or enhanced + choices: + - basic + - enhanced + type: str + da: + description: + - List of device-alias to be added or removed + type: list + elements: dict + suboptions: + name: + description: + - Name of the device-alias to be added or removed + required: true + type: str + pwwn: + description: + - pwwn to which the name needs to be associated with + type: str + remove: + description: + - Removes the device-alias if set to True + type: bool + default: false + rename: + description: + - List of device-alias to be renamed + type: list + elements: dict + suboptions: + old_name: + description: + - Old name of the device-alias that needs to be renamed + required: true + type: str + new_name: + description: + - New name of the device-alias + required: true + type: str +""" + +EXAMPLES = """ +- name: Test that device alias module works + cisco.nxos.nxos_devicealias: + da: + - name: test1_add + pwwn: 56:2:22:11:22:88:11:67 + - name: test2_add + pwwn: 65:22:22:11:22:22:11:d + - name: dev1 + remove: true + - name: dev2 + remove: true + distribute: true + mode: enhanced + rename: + - new_name: bcd + old_name: abc + - new_name: bcd1 + old_name: abc1 + + +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - device-alias database + - device-alias name somename pwwn 10:00:00:00:89:a1:01:03 + - device-alias name somename1 pwwn 10:00:00:00:89:a1:02:03 + - device-alias commit + - no terminal dont-ask +""" + +import string + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + +VALID_DA_CHARS = ("-", "_", "$", "^") + + +class showDeviceAliasStatus(object): + """docstring for showDeviceAliasStatus""" + + def __init__(self, module): + self.module = module + self.distribute = "" + self.mode = "" + self.locked = False + self.update() + + def execute_show_cmd(self, cmd): + output = execute_show_command(cmd, self.module)[0] + return output + + def update(self): + command = "show device-alias status" + output = self.execute_show_cmd(command).split("\n") + for o in output: + if "Fabric Distribution" in o: + self.distribute = o.split(":")[1].strip().lower() + if "Mode" in o: + self.mode = o.split("Mode:")[1].strip().lower() + if "Locked" in o: + self.locked = True + + def isLocked(self): + return self.locked + + def getDistribute(self): + return self.distribute + + def getMode(self): + return self.mode + + +class showDeviceAliasDatabase(object): + """docstring for showDeviceAliasDatabase""" + + def __init__(self, module): + self.module = module + self.da_dict = {} + self.update() + + def execute_show_cmd(self, cmd): + output = execute_show_command(cmd, self.module)[0] + return output + + def update(self): + command = "show device-alias database" + # output = execute_show_command(command, self.module)[0].split("\n") + output = self.execute_show_cmd(command) + self.da_list = output.split("\n") + for eachline in self.da_list: + if "device-alias" in eachline: + sv = eachline.strip().split() + self.da_dict[sv[2]] = sv[4] + + def isNameInDaDatabase(self, name): + return name in self.da_dict.keys() + + def isPwwnInDaDatabase(self, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + return newpwwn in self.da_dict.values() + + def isNamePwwnPresentInDatabase(self, name, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + if name in self.da_dict.keys(): + if newpwwn == self.da_dict[name]: + return True + return False + + def getPwwnByName(self, name): + if name in self.da_dict.keys(): + return self.da_dict[name] + else: + return None + + def getNameByPwwn(self, pwwn): + newpwwn = ":".join(["0" + str(ep) if len(ep) == 1 else ep for ep in pwwn.split(":")]) + for n, p in self.da_dict.items(): + if p == newpwwn: + return n + return None + + +def isPwwnValid(pwwn): + pwwnsplit = pwwn.split(":") + if len(pwwnsplit) != 8: + return False + for eachpwwnsplit in pwwnsplit: + if len(eachpwwnsplit) > 2 or len(eachpwwnsplit) < 1: + return False + if not all(c in string.hexdigits for c in eachpwwnsplit): + return False + return True + + +def isNameValid(name): + if not name[0].isalpha(): + # Illegal first character. Name must start with a letter + return False + if len(name) > 64: + return False + for character in name: + if not character.isalnum() and character not in VALID_DA_CHARS: + return False + return True + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + out = run_commands(module, commands) + return out + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def main(): + element_spec = dict( + name=dict(required=True, type="str"), + pwwn=dict(type="str"), + remove=dict(type="bool", default=False), + ) + + element_spec_rename = dict( + old_name=dict(required=True, type="str"), + new_name=dict(required=True, type="str"), + ) + + argument_spec = dict( + distribute=dict(type="bool"), + mode=dict(type="str", choices=["enhanced", "basic"]), + da=dict(type="list", elements="dict", options=element_spec), + rename=dict(type="list", elements="dict", options=element_spec_rename), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + messages = list() + commands_to_execute = list() + result = {"changed": False} + + distribute = module.params["distribute"] + mode = module.params["mode"] + da = module.params["da"] + rename = module.params["rename"] + + # Step 0.0: Validate syntax of name and pwwn + # Also validate syntax of rename arguments + if da is not None: + for eachdict in da: + name = eachdict["name"] + pwwn = eachdict["pwwn"] + remove = eachdict["remove"] + if pwwn is not None: + pwwn = pwwn.lower() + if not remove: + if pwwn is None: + module.fail_json( + msg="This device alias name " + + str(name) + + " which needs to be added, does not have pwwn specified. Please specify a valid pwwn", + ) + if not isNameValid(name): + module.fail_json( + msg="This pwwn name is invalid : " + + str(name) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + if not isPwwnValid(pwwn): + module.fail_json( + msg="This pwwn is invalid : " + + str(pwwn) + + ". Please check that its a valid pwwn", + ) + if rename is not None: + for eachdict in rename: + oldname = eachdict["old_name"] + newname = eachdict["new_name"] + if not isNameValid(oldname): + module.fail_json( + msg="This pwwn name is invalid : " + + str(oldname) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + if not isNameValid(newname): + module.fail_json( + msg="This pwwn name is invalid : " + + str(newname) + + ". Note that name cannot be more than 64 alphanumeric chars, " + + "it must start with a letter, and can only contain these characters: " + + ", ".join(["'{0}'".format(c) for c in VALID_DA_CHARS]), + ) + + # Step 0.1: Check DA status + shDAStausObj = showDeviceAliasStatus(module) + d = shDAStausObj.getDistribute() + m = shDAStausObj.getMode() + if shDAStausObj.isLocked(): + module.fail_json(msg="device-alias has acquired lock on the switch. Hence cannot procced.") + + # Step 1: Process distribute + commands = [] + if distribute is not None: + if distribute: + # playbook has distribute as True(enabled) + if d == "disabled": + # but switch distribute is disabled(false), so set it to + # true(enabled) + commands.append("device-alias distribute") + messages.append("device-alias distribute changed from disabled to enabled") + else: + messages.append( + "device-alias distribute remains unchanged. current distribution mode is enabled", + ) + else: + # playbook has distribute as False(disabled) + if d == "enabled": + # but switch distribute is enabled(true), so set it to + # false(disabled) + commands.append("no device-alias distribute") + messages.append("device-alias distribute changed from enabled to disabled") + else: + messages.append( + "device-alias distribute remains unchanged. current distribution mode is disabled", + ) + + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the da_add/da_remove stage + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step 2: Process mode + commands = [] + if mode is not None: + if mode == "basic": + # playbook has mode as basic + if m == "enhanced": + # but switch mode is enhanced, so set it to basic + commands.append("no device-alias mode enhanced") + messages.append("device-alias mode changed from enhanced to basic") + else: + messages.append("device-alias mode remains unchanged. current mode is basic") + + else: + # playbook has mode as enhanced + if m == "basic": + # but switch mode is basic, so set it to enhanced + commands.append("device-alias mode enhanced") + messages.append("device-alias mode changed from basic to enhanced") + else: + messages.append("device-alias mode remains unchanged. current mode is enhanced") + + if commands: + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step 3: Process da + commands = [] + shDADatabaseObj = showDeviceAliasDatabase(module) + if da is not None: + da_remove_list = [] + da_add_list = [] + for eachdict in da: + name = eachdict["name"] + pwwn = eachdict["pwwn"] + remove = eachdict["remove"] + if pwwn is not None: + pwwn = pwwn.lower() + if remove: + if shDADatabaseObj.isNameInDaDatabase(name): + commands.append("no device-alias name " + name) + da_remove_list.append(name) + else: + messages.append( + name + + " - This device alias name is not in switch device-alias database, hence cannot be removed.", + ) + else: + if shDADatabaseObj.isNamePwwnPresentInDatabase(name, pwwn): + messages.append( + name + + " : " + + pwwn + + " - This device alias name,pwwn is already in switch device-alias database, hence nothing to configure", + ) + else: + if shDADatabaseObj.isNameInDaDatabase(name): + module.fail_json( + msg=name + + " - This device alias name is already present in switch device-alias database but assigned to another pwwn (" + + shDADatabaseObj.getPwwnByName(name) + + ") hence cannot be added", + ) + + elif shDADatabaseObj.isPwwnInDaDatabase(pwwn): + module.fail_json( + msg=pwwn + + " - This device alias pwwn is already present in switch device-alias database but assigned to another name (" + + shDADatabaseObj.getNameByPwwn(pwwn) + + ") hence cannot be added", + ) + + else: + commands.append("device-alias name " + name + " pwwn " + pwwn) + da_add_list.append(name) + + if len(da_add_list) != 0 or len(da_remove_list) != 0: + commands = ["device-alias database"] + commands + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + if len(da_remove_list) != 0: + messages.append( + "the required device-alias were removed. " + ",".join(da_remove_list), + ) + if len(da_add_list) != 0: + messages.append( + "the required device-alias were added. " + ",".join(da_add_list), + ) + + # Step 5: Process rename + commands = [] + if rename is not None: + for eachdict in rename: + oldname = eachdict["old_name"] + newname = eachdict["new_name"] + if shDADatabaseObj.isNameInDaDatabase(newname): + module.fail_json( + changed=False, + commands=cmds, + msg=newname + + " - this name is already present in the device-alias database, hence we cannot rename " + + oldname + + " with this one", + ) + if shDADatabaseObj.isNameInDaDatabase(oldname): + commands.append("device-alias rename " + oldname + " " + newname) + else: + module.fail_json( + changed=False, + commands=cmds, + msg=oldname + + " - this name is not present in the device-alias database, hence we cannot rename.", + ) + + if len(commands) != 0: + commands = ["device-alias database"] + commands + if distribute: + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + else: + if distribute is None and d == "enabled": + commands.append("device-alias commit") + commands = ["terminal dont-ask"] + commands + ["no terminal dont-ask"] + cmds = flatten_list(commands) + if cmds: + commands_to_execute = commands_to_execute + cmds + if module.check_mode: + # Check mode implemented at the end + pass + else: + result["changed"] = True + load_config(module, cmds) + + # Step END: check for 'check' mode + if module.check_mode: + module.exit_json( + changed=False, + commands=commands_to_execute, + msg="Check Mode: No cmds issued to the hosts", + ) + + result["messages"] = messages + result["commands"] = commands_to_execute + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_vsan.py b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_vsan.py new file mode 100644 index 00000000..d95d95a9 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_vsan.py @@ -0,0 +1,354 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_vsan +short_description: Configuration of vsan for Cisco NXOS MDS Switches. +description: +- Configuration of vsan for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + vsan: + description: + - List of vsan details to be added or removed + type: list + elements: dict + suboptions: + id: + description: + - Vsan id + required: true + type: int + name: + description: + - Name of the vsan + type: str + suspend: + description: + - suspend the vsan if True + type: bool + remove: + description: + - Removes the vsan if True + type: bool + interface: + description: + - List of vsan's interfaces to be added + type: list + elements: str +""" + +EXAMPLES = """ +- name: Test that vsan module works + cisco.nxos.nxos_vsan: + vsan: + - id: 922 + interface: + - fc1/1 + - fc1/2 + - port-channel 1 + name: vsan-SAN-A + remove: false + suspend: false + - id: 923 + interface: + - fc1/11 + - fc1/21 + - port-channel 2 + name: vsan-SAN-B + remove: false + suspend: true + - id: 1923 + name: vsan-SAN-Old + remove: true +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - vsan database + - vsan 922 interface fc1/40 + - vsan 922 interface port-channel 155 + - no terminal dont-ask +""" + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + + +class Vsan(object): + def __init__(self, vsanid): + self.vsanid = vsanid + self.vsanname = None + self.vsanstate = None + self.vsanoperstate = None + self.vsaninterfaces = [] + + +class GetVsanInfoFromSwitch(object): + """docstring for GetVsanInfoFromSwitch""" + + def __init__(self, module): + self.module = module + self.vsaninfo = {} + self.processShowVsan() + self.processShowVsanMembership() + + def execute_show_vsan_cmd(self): + output = execute_show_command("show vsan", self.module)[0] + return output + + def execute_show_vsan_mem_cmd(self): + output = execute_show_command("show vsan membership", self.module)[0] + return output + + def processShowVsan(self): + patv = r"^vsan\s+(\d+)\s+information" + patnamestate = "name:(.*)state:(.*)" + patoperstate = "operational state:(.*)" + + output = self.execute_show_vsan_cmd().split("\n") + for o in output: + z = re.match(patv, o.strip()) + if z: + v = z.group(1).strip() + self.vsaninfo[v] = Vsan(v) + + z1 = re.match(patnamestate, o.strip()) + if z1: + n = z1.group(1).strip() + s = z1.group(2).strip() + self.vsaninfo[v].vsanname = n + self.vsaninfo[v].vsanstate = s + + z2 = re.match(patoperstate, o.strip()) + if z2: + oper = z2.group(1).strip() + self.vsaninfo[v].vsanoperstate = oper + + # 4094/4079 vsan is always present + self.vsaninfo["4079"] = Vsan("4079") + self.vsaninfo["4094"] = Vsan("4094") + + def processShowVsanMembership(self): + patv = r"^vsan\s+(\d+).*" + output = self.execute_show_vsan_mem_cmd().split("\n") + memlist = [] + v = None + for o in output: + z = re.match(patv, o.strip()) + if z: + if v is not None: + self.vsaninfo[v].vsaninterfaces = memlist + memlist = [] + v = z.group(1) + if "interfaces" not in o: + llist = o.strip().split() + memlist = memlist + llist + self.vsaninfo[v].vsaninterfaces = memlist + + def getVsanInfoObjects(self): + return self.vsaninfo + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + return run_commands(module, commands) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def main(): + vsan_element_spec = dict( + id=dict(required=True, type="int"), + name=dict(type="str"), + remove=dict(type="bool"), + suspend=dict(type="bool"), + interface=dict(type="list", elements="str"), + ) + + argument_spec = dict(vsan=dict(type="list", elements="dict", options=vsan_element_spec)) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + warnings = list() + messages = list() + commands_executed = list() + result = {"changed": False} + + obj = GetVsanInfoFromSwitch(module) + dictSwVsanObjs = obj.getVsanInfoObjects() + + commands = [] + vsan_list = module.params["vsan"] + + for eachvsan in vsan_list: + vsanid = str(eachvsan["id"]) + vsanname = eachvsan["name"] + vsanremove = eachvsan["remove"] + vsansuspend = eachvsan["suspend"] + vsaninterface_list = eachvsan["interface"] + + if int(vsanid) < 1 or int(vsanid) >= 4095: + module.fail_json( + msg=vsanid + " - This is an invalid vsan. Supported vsan range is 1-4094", + ) + + if vsanid in dictSwVsanObjs.keys(): + sw_vsanid = vsanid + sw_vsanname = dictSwVsanObjs[vsanid].vsanname + sw_vsanstate = dictSwVsanObjs[vsanid].vsanstate + sw_vsaninterfaces = dictSwVsanObjs[vsanid].vsaninterfaces + else: + sw_vsanid = None + sw_vsanname = None + sw_vsanstate = None + sw_vsaninterfaces = [] + + if vsanremove: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, hence cannot be removed") + continue + if vsanid == sw_vsanid: + commands.append("no vsan " + str(vsanid)) + messages.append("deleting the vsan " + str(vsanid)) + else: + messages.append( + "There is no vsan " + + str(vsanid) + + " present in the switch. Hence there is nothing to delete", + ) + continue + else: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append( + str(vsanid) + " is a reserved vsan, and always present on the switch", + ) + else: + if vsanid == sw_vsanid: + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch. Hence there is nothing to configure", + ) + else: + commands.append("vsan " + str(vsanid)) + messages.append("creating vsan " + str(vsanid)) + + if vsanname is not None: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, and cannot be renamed") + else: + if vsanname == sw_vsanname: + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which has the name " + + vsanname + + " Hence there is nothing to configure", + ) + else: + commands.append("vsan " + str(vsanid) + " name " + vsanname) + messages.append("setting vsan name to " + vsanname + " for vsan " + str(vsanid)) + + if vsansuspend: + # Negative case: + if vsanid == "4079" or vsanid == "4094": + messages.append(str(vsanid) + " is a reserved vsan, and cannot be suspended") + else: + if sw_vsanstate == "suspended": + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which is in suspended state ", + ) + else: + commands.append("vsan " + str(vsanid) + " suspend") + messages.append("suspending the vsan " + str(vsanid)) + else: + if sw_vsanstate == "active": + messages.append( + "There is already a vsan " + + str(vsanid) + + " present in the switch, which is in active state ", + ) + else: + commands.append("no vsan " + str(vsanid) + " suspend") + messages.append("no suspending the vsan " + str(vsanid)) + + if vsaninterface_list is not None: + for each_interface_name in vsaninterface_list: + # For fcip,port-channel,vfc-port-channel need to remove the + # extra space to compare + temp = re.sub(" +", "", each_interface_name) + if temp in sw_vsaninterfaces: + messages.append( + each_interface_name + + " is already present in the vsan " + + str(vsanid) + + " interface list", + ) + else: + commands.append("vsan " + str(vsanid) + " interface " + each_interface_name) + messages.append( + "adding interface " + each_interface_name + " to vsan " + str(vsanid), + ) + + if len(commands) != 0: + commands = ["terminal dont-ask"] + ["vsan database"] + commands + ["no terminal dont-ask"] + + cmds = flatten_list(commands) + commands_executed = cmds + + if commands_executed: + if module.check_mode: + module.exit_json( + changed=False, + commands=commands_executed, + msg="Check Mode: No cmds issued to the hosts", + ) + else: + result["changed"] = True + load_config(module, commands_executed) + + result["messages"] = messages + result["commands"] = commands_executed + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() diff --git a/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_zone_zoneset.py b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_zone_zoneset.py new file mode 100644 index 00000000..7c9fba30 --- /dev/null +++ b/ansible_collections/cisco/nxos/plugins/modules/storage/nxos_zone_zoneset.py @@ -0,0 +1,888 @@ +#!/usr/bin/python +# Copyright: Ansible Project +# 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 + + +DOCUMENTATION = """ +module: nxos_zone_zoneset +short_description: Configuration of zone/zoneset for Cisco NXOS MDS Switches. +description: +- Configuration of zone/zoneset for Cisco MDS NXOS. +version_added: 1.0.0 +author: +- Suhas Bharadwaj (@srbharadwaj) (subharad@cisco.com) +notes: +- Tested against Cisco MDS NX-OS 8.4(1) +options: + zone_zoneset_details: + description: + - List of zone/zoneset details to be added or removed + type: list + elements: dict + suboptions: + vsan: + description: + - vsan id + required: true + type: int + mode: + description: + - mode of the zone for the vsan + choices: + - enhanced + - basic + type: str + default_zone: + description: + - default zone behaviour for the vsan + choices: + - permit + - deny + type: str + smart_zoning: + description: + - Removes the vsan if True + type: bool + zone: + description: + - List of zone options for that vsan + type: list + elements: dict + suboptions: + name: + description: + - name of the zone + required: true + type: str + remove: + description: + - Deletes the zone if True + type: bool + default: false + members: + description: + - Members of the zone that needs to be removed or added + type: list + elements: dict + suboptions: + pwwn: + description: + - pwwn member of the zone, use alias 'device_alias' as option for + device_alias member + aliases: + - device_alias + required: true + type: str + remove: + description: + - Removes member from the zone if True + type: bool + default: false + devtype: + description: + - devtype of the zone member used along with Smart zoning config + choices: + - initiator + - target + - both + type: str + zoneset: + description: + - List of zoneset options for the vsan + type: list + elements: dict + suboptions: + name: + description: + - name of the zoneset + required: true + type: str + remove: + description: + - Removes zoneset if True + type: bool + default: false + action: + description: + - activates/de-activates the zoneset + choices: + - activate + - deactivate + type: str + members: + description: + - Members of the zoneset that needs to be removed or added + type: list + elements: dict + suboptions: + name: + description: + - name of the zone that needs to be added to the zoneset or removed + from the zoneset + required: true + type: str + remove: + description: + - Removes zone member from the zoneset + type: bool + default: false +""" + +EXAMPLES = """ +- name: Test that zone/zoneset module works + cisco.nxos.nxos_zone_zoneset: + zone_zoneset_details: + - mode: enhanced + vsan: 22 + zone: + - members: + - pwwn: 11:11:11:11:11:11:11:11 + - device_alias: test123 + - pwwn: 61:61:62:62:12:12:12:12 + remove: true + name: zoneA + - members: + - pwwn: 10:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:21:21:21:21 + name: zoneB + - name: zoneC + remove: true + zoneset: + - action: activate + members: + - name: zoneA + - name: zoneB + - name: zoneC + remove: true + name: zsetname1 + - action: deactivate + name: zsetTestExtra + remove: true + - mode: basic + smart_zoning: true + vsan: 21 + zone: + - members: + - devtype: both + pwwn: 11:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:12:12:12:12 + - devtype: both + pwwn: 92:62:62:62:12:12:1a:1a + remove: true + name: zone21A + - members: + - pwwn: 10:11:11:11:11:11:11:11 + - pwwn: 62:62:62:62:21:21:21:21 + name: zone21B + zoneset: + - action: activate + members: + - name: zone21A + - name: zone21B + name: zsetname212 + +""" + +RETURN = """ +commands: + description: commands sent to the device + returned: always + type: list + sample: + - terminal dont-ask + - zone name zoneA vsan 923 + - member pwwn 11:11:11:11:11:11:11:11 + - no member device-alias test123 + - zone commit vsan 923 + - no terminal dont-ask +messages: + description: debug messages + returned: always + type: list + sample: + - "zone mode is already enhanced ,no change in zone mode configuration for vsan 922" + - "zone member '11:11:11:11:11:11:11:11' is already present in zone 'zoneA' in vsan 922 hence nothing to add" + - "zone member 'test123' is already present in zone 'zoneA' in vsan 922 hence nothing to add" + - "zone member '61:61:62:62:12:12:12:12' is not present in zone 'zoneA' in vsan 922 hence nothing to remove" + - "zone member '10:11:11:11:11:11:11:11' is already present in zone 'zoneB' in vsan 922 hence nothing to add" + - "zone member '62:62:62:62:21:21:21:21' is already present in zone 'zoneB' in vsan 922 hence nothing to add" + - "zone 'zoneC' is not present in vsan 922 , so nothing to remove" +""" + + +import re + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( + load_config, + run_commands, +) + + +__metaclass__ = type + + +class ShowZonesetActive(object): + """docstring for ShowZonesetActive""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.activeZSName = None + self.parseCmdOutput() + + def execute_show_zoneset_active_cmd(self): + command = "show zoneset active vsan " + str(self.vsan) + " | grep zoneset" + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZoneset = r"zoneset name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zoneset_active_cmd().split("\n") + if len(output) == 0: + return + else: + for line in output: + line = line.strip() + mzs = re.match(patZoneset, line.strip()) + if mzs: + self.activeZSName = mzs.group(1).strip() + return + + def isZonesetActive(self, zsname): + if zsname == self.activeZSName: + return True + return False + + +class ShowZoneset(object): + """docstring for ShowZoneset""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.zsDetails = {} + self.parseCmdOutput() + + def execute_show_zoneset_cmd(self): + command = "show zoneset vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZoneset = r"zoneset name (\S+) vsan " + str(self.vsan) + patZone = r"zone name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zoneset_cmd().split("\n") + for line in output: + line = line.strip() + mzs = re.match(patZoneset, line.strip()) + mz = re.match(patZone, line.strip()) + if mzs: + zonesetname = mzs.group(1).strip() + self.zsDetails[zonesetname] = [] + continue + elif mz: + zonename = mz.group(1).strip() + v = self.zsDetails[zonesetname] + v.append(zonename) + self.zsDetails[zonesetname] = v + + def isZonesetPresent(self, zsname): + return zsname in self.zsDetails.keys() + + def isZonePresentInZoneset(self, zsname, zname): + if zsname in self.zsDetails.keys(): + return zname in self.zsDetails[zsname] + return False + + +class ShowZone(object): + """docstring for ShowZone""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.module = module + self.zDetails = {} + self.parseCmdOutput() + + def execute_show_zone_vsan_cmd(self): + command = "show zone vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def parseCmdOutput(self): + patZone = r"zone name (\S+) vsan " + str(self.vsan) + output = self.execute_show_zone_vsan_cmd().split("\n") + for line in output: + line = re.sub(r"[\[].*?[\]]", "", line) + line = " ".join(line.strip().split()) + if "init" in line: + line = line.replace("init", "initiator") + m = re.match(patZone, line) + if m: + zonename = m.group(1).strip() + self.zDetails[zonename] = [] + continue + else: + # For now we support only pwwn and device-alias under zone + # Ideally should use 'supported_choices'....but maybe next + # time. + if "pwwn" in line or "device-alias" in line: + v = self.zDetails[zonename] + v.append(line) + self.zDetails[zonename] = v + + def isZonePresent(self, zname): + return zname in self.zDetails.keys() + + def isZoneMemberPresent(self, zname, cmd): + if zname in self.zDetails.keys(): + zonememlist = self.zDetails[zname] + for eachline in zonememlist: + if cmd in eachline: + return True + return False + + def get_zDetails(self): + return self.zDetails + + +class ShowZoneStatus(object): + """docstring for ShowZoneStatus""" + + def __init__(self, module, vsan): + self.vsan = vsan + self.vsanAbsent = False + self.module = module + self.default_zone = "" + self.mode = "" + self.session = "" + self.sz = "" + self.locked = False + self.update() + + def execute_show_zone_status_cmd(self): + command = "show zone status vsan " + str(self.vsan) + output = execute_show_command(command, self.module)[0] + return output + + def update(self): + output = self.execute_show_zone_status_cmd().split("\n") + + patfordefzone = "VSAN: " + str(self.vsan) + r" default-zone:\s+(\S+).*" + patformode = r".*mode:\s+(\S+).*" + patforsession = r"^session:\s+(\S+).*" + patforsz = r".*smart-zoning:\s+(\S+).*" + for line in output: + if "is not configured" in line: + self.vsanAbsent = True + break + mdefz = re.match(patfordefzone, line.strip()) + mmode = re.match(patformode, line.strip()) + msession = re.match(patforsession, line.strip()) + msz = re.match(patforsz, line.strip()) + + if mdefz: + self.default_zone = mdefz.group(1) + if mmode: + self.mode = mmode.group(1) + if msession: + self.session = msession.group(1) + if self.session != "none": + self.locked = True + if msz: + self.sz = msz.group(1) + + def isLocked(self): + return self.locked + + def getDefaultZone(self): + return self.default_zone + + def getMode(self): + return self.mode + + def getSmartZoningStatus(self): + return self.sz + + def isVsanAbsent(self): + return self.vsanAbsent + + +def execute_show_command(command, module, command_type="cli_show"): + output = "text" + commands = [{"command": command, "output": output}] + return run_commands(module, commands) + + +def flatten_list(command_lists): + flat_command_list = [] + for command in command_lists: + if isinstance(command, list): + flat_command_list.extend(command) + else: + flat_command_list.append(command) + return flat_command_list + + +def getMemType(supported_choices, allmemkeys, default="pwwn"): + for eachchoice in supported_choices: + if eachchoice in allmemkeys: + return eachchoice + return default + + +def main(): + supported_choices = ["device_alias"] + zone_member_spec = dict( + pwwn=dict(required=True, type="str", aliases=["device_alias"]), + devtype=dict(type="str", choices=["initiator", "target", "both"]), + remove=dict(type="bool", default=False), + ) + + zone_spec = dict( + name=dict(required=True, type="str"), + members=dict(type="list", elements="dict", options=zone_member_spec), + remove=dict(type="bool", default=False), + ) + + zoneset_member_spec = dict( + name=dict(required=True, type="str"), + remove=dict(type="bool", default=False), + ) + + zoneset_spec = dict( + name=dict(type="str", required=True), + members=dict(type="list", elements="dict", options=zoneset_member_spec), + remove=dict(type="bool", default=False), + action=dict(type="str", choices=["activate", "deactivate"]), + ) + + zonedetails_spec = dict( + vsan=dict(required=True, type="int"), + mode=dict(type="str", choices=["enhanced", "basic"]), + default_zone=dict(type="str", choices=["permit", "deny"]), + smart_zoning=dict(type="bool"), + zone=dict(type="list", elements="dict", options=zone_spec), + zoneset=dict(type="list", elements="dict", options=zoneset_spec), + ) + + argument_spec = dict( + zone_zoneset_details=dict(type="list", elements="dict", options=zonedetails_spec), + ) + + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + + warnings = list() + messages = list() + commands = list() + result = {"changed": False} + + commands_executed = [] + listOfZoneDetails = module.params["zone_zoneset_details"] + for eachZoneZonesetDetail in listOfZoneDetails: + vsan = eachZoneZonesetDetail["vsan"] + op_mode = eachZoneZonesetDetail["mode"] + op_default_zone = eachZoneZonesetDetail["default_zone"] + op_smart_zoning = eachZoneZonesetDetail["smart_zoning"] + op_zone = eachZoneZonesetDetail["zone"] + op_zoneset = eachZoneZonesetDetail["zoneset"] + + # Step1: execute show zone status and get + shZoneStatusObj = ShowZoneStatus(module, vsan) + sw_default_zone = shZoneStatusObj.getDefaultZone() + sw_mode = shZoneStatusObj.getMode() + sw_smart_zoning = shZoneStatusObj.getSmartZoningStatus() + + if sw_smart_zoning.lower() == "Enabled".lower(): + sw_smart_zoning_bool = True + else: + sw_smart_zoning_bool = False + + if shZoneStatusObj.isVsanAbsent(): + module.fail_json( + msg="Vsan " + str(vsan) + " is not present in the switch. Hence cannot procced.", + ) + + if shZoneStatusObj.isLocked(): + module.fail_json( + msg="zone has acquired lock on the switch for vsan " + + str(vsan) + + ". Hence cannot procced.", + ) + + # Process zone default zone options + if op_default_zone is not None: + if op_default_zone != sw_default_zone: + if op_default_zone == "permit": + commands_executed.append("zone default-zone permit vsan " + str(vsan)) + messages.append( + "default zone configuration changed from deny to permit for vsan " + + str(vsan), + ) + else: + commands_executed.append("no zone default-zone permit vsan " + str(vsan)) + messages.append( + "default zone configuration changed from permit to deny for vsan " + + str(vsan), + ) + else: + messages.append( + "default zone is already " + + op_default_zone + + " ,no change in default zone configuration for vsan " + + str(vsan), + ) + + # Process zone mode options + if op_mode is not None: + if op_mode != sw_mode: + if op_mode == "enhanced": + commands_executed.append("zone mode enhanced vsan " + str(vsan)) + messages.append( + "zone mode configuration changed from basic to enhanced for vsan " + + str(vsan), + ) + else: + commands_executed.append("no zone mode enhanced vsan " + str(vsan)) + messages.append( + "zone mode configuration changed from enhanced to basic for vsan " + + str(vsan), + ) + else: + messages.append( + "zone mode is already " + + op_mode + + " ,no change in zone mode configuration for vsan " + + str(vsan), + ) + + # Process zone smart-zone options + if op_smart_zoning is not None: + if op_smart_zoning != sw_smart_zoning_bool: + if op_smart_zoning: + commands_executed.append("zone smart-zoning enable vsan " + str(vsan)) + messages.append("smart-zoning enabled for vsan " + str(vsan)) + else: + commands_executed.append("no zone smart-zoning enable vsan " + str(vsan)) + messages.append("smart-zoning disabled for vsan " + str(vsan)) + else: + messages.append( + "smart-zoning is already set to " + + sw_smart_zoning + + " , no change in smart-zoning configuration for vsan " + + str(vsan), + ) + + # Process zone member options + # TODO: Obviously this needs to be cleaned up properly, as there are a lot of ifelse statements which is bad + # Will take it up later becoz of time constraints + if op_zone is not None: + shZoneObj = ShowZone(module, vsan) + for eachzone in op_zone: + zname = eachzone["name"] + zmembers = eachzone["members"] + removeflag = eachzone["remove"] + if removeflag: + if shZoneObj.isZonePresent(zname): + messages.append("zone '" + zname + "' is removed from vsan " + str(vsan)) + commands_executed.append("no zone name " + zname + " vsan " + str(vsan)) + else: + messages.append( + "zone '" + + zname + + "' is not present in vsan " + + str(vsan) + + " , so nothing to remove", + ) + else: + if zmembers is None: + if shZoneObj.isZonePresent(zname): + messages.append( + "zone '" + zname + "' is already present in vsan " + str(vsan), + ) + else: + commands_executed.append("zone name " + zname + " vsan " + str(vsan)) + messages.append("zone '" + zname + "' is created in vsan " + str(vsan)) + else: + cmdmemlist = [] + for eachmem in zmembers: + memtype = getMemType(supported_choices, eachmem.keys()) + cmd = memtype.replace("_", "-") + " " + eachmem[memtype] + if op_smart_zoning or sw_smart_zoning_bool: + if eachmem["devtype"] is not None: + cmd = cmd + " " + eachmem["devtype"] + if eachmem["remove"]: + if shZoneObj.isZonePresent(zname): + if shZoneObj.isZoneMemberPresent(zname, cmd): + cmd = "no member " + cmd + cmdmemlist.append(cmd) + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "removing zone member '" + + eachmem[memtype] + + " of device type '" + + eachmem["devtype"] + + "' from zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "removing zone member '" + + eachmem[memtype] + + "' from zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' is not present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to remove", + ) + else: + messages.append( + "zone member '" + + eachmem[memtype] + + "' is not present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to remove", + ) + else: + messages.append( + "zone '" + + zname + + "' is not present in vsan " + + str(vsan) + + " , hence cannot remove the members", + ) + + else: + if shZoneObj.isZoneMemberPresent(zname, cmd): + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' is already present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to add", + ) + else: + messages.append( + "zone member '" + + eachmem[memtype] + + "' is already present in zone '" + + zname + + "' in vsan " + + str(vsan) + + " hence nothing to add", + ) + else: + cmd = "member " + cmd + cmdmemlist.append(cmd) + if op_smart_zoning and eachmem["devtype"] is not None: + messages.append( + "adding zone member '" + + eachmem[memtype] + + "' of device type '" + + eachmem["devtype"] + + "' to zone '" + + zname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "adding zone member '" + + eachmem[memtype] + + "' to zone '" + + zname + + "' in vsan " + + str(vsan), + ) + if len(cmdmemlist) != 0: + commands_executed.append("zone name " + zname + " vsan " + str(vsan)) + commands_executed = commands_executed + cmdmemlist + + # Process zoneset member options + if op_zoneset is not None: + dactcmd = [] + actcmd = [] + shZonesetObj = ShowZoneset(module, vsan) + shZonesetActiveObj = ShowZonesetActive(module, vsan) + for eachzoneset in op_zoneset: + zsetname = eachzoneset["name"] + zsetmembers = eachzoneset["members"] + removeflag = eachzoneset["remove"] + actionflag = eachzoneset["action"] + if removeflag: + if shZonesetObj.isZonesetPresent(zsetname): + messages.append( + "zoneset '" + zsetname + "' is removed from vsan " + str(vsan), + ) + commands_executed.append( + "no zoneset name " + zsetname + " vsan " + str(vsan), + ) + else: + messages.append( + "zoneset '" + + zsetname + + "' is not present in vsan " + + str(vsan) + + " ,hence there is nothing to remove", + ) + else: + if zsetmembers is not None: + cmdmemlist = [] + for eachzsmem in zsetmembers: + zsetmem_name = eachzsmem["name"] + zsetmem_removeflag = eachzsmem["remove"] + if zsetmem_removeflag: + if shZonesetObj.isZonePresentInZoneset(zsetname, zsetmem_name): + cmd = "no member " + zsetmem_name + cmdmemlist.append(cmd) + messages.append( + "removing zoneset member '" + + zsetmem_name + + "' from zoneset '" + + zsetname + + "' in vsan " + + str(vsan), + ) + else: + messages.append( + "zoneset member '" + + zsetmem_name + + "' is not present in zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " ,hence there is nothing to remove", + ) + else: + if shZonesetObj.isZonePresentInZoneset(zsetname, zsetmem_name): + messages.append( + "zoneset member '" + + zsetmem_name + + "' is already present in zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " ,hence there is nothing to add", + ) + else: + cmd = "member " + zsetmem_name + cmdmemlist.append(cmd) + messages.append( + "adding zoneset member '" + + zsetmem_name + + "' to zoneset '" + + zsetname + + "' in vsan " + + str(vsan), + ) + if len(cmdmemlist) != 0: + commands_executed.append( + "zoneset name " + zsetname + " vsan " + str(vsan), + ) + commands_executed = commands_executed + cmdmemlist + else: + if shZonesetObj.isZonesetPresent(zsetname): + messages.append( + "zoneset '" + + zsetname + + "' is already present in vsan " + + str(vsan), + ) + else: + commands_executed.append( + "zoneset name " + zsetname + " vsan " + str(vsan), + ) + messages.append( + "zoneset '" + zsetname + "' is created in vsan " + str(vsan), + ) + + # Process zoneset activate options + if actionflag == "deactivate": + if shZonesetActiveObj.isZonesetActive(zsetname): + messages.append( + "deactivating zoneset '" + zsetname + "' in vsan " + str(vsan), + ) + dactcmd.append( + "no zoneset activate name " + zsetname + " vsan " + str(vsan), + ) + else: + messages.append( + "zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " is not activated, hence cannot deactivate", + ) + elif actionflag == "activate": + if commands_executed: + messages.append( + "activating zoneset '" + zsetname + "' in vsan " + str(vsan), + ) + actcmd.append("zoneset activate name " + zsetname + " vsan " + str(vsan)) + else: + messages.append( + "no changes to existing zoneset '" + + zsetname + + "' in vsan " + + str(vsan) + + " hence activate action is ignored", + ) + commands_executed = commands_executed + dactcmd + actcmd + + if commands_executed: + if op_mode == "enhanced": + commands_executed.append("zone commit vsan " + str(vsan)) + elif op_mode is None: + if sw_mode == "enhanced": + commands_executed.append("zone commit vsan " + str(vsan)) + + if commands_executed: + commands_executed = ["terminal dont-ask"] + commands_executed + ["no terminal dont-ask"] + + cmds = flatten_list(commands_executed) + if cmds: + if module.check_mode: + module.exit_json( + changed=False, + commands=cmds, + msg="Check Mode: No cmds issued to the hosts", + ) + else: + result["changed"] = True + commands = commands + cmds + load_config(module, cmds) + + result["messages"] = messages + result["commands"] = commands_executed + result["warnings"] = warnings + module.exit_json(**result) + + +if __name__ == "__main__": + main() |