diff --git a/lib/ansible/module_utils/network/nxos/nxos.py b/lib/ansible/module_utils/network/nxos/nxos.py index 1b54648dec..dfbb7f1e86 100644 --- a/lib/ansible/module_utils/network/nxos/nxos.py +++ b/lib/ansible/module_utils/network/nxos/nxos.py @@ -38,9 +38,24 @@ from ansible.module_utils.network.common.utils import to_list, ComplexList from ansible.module_utils.connection import Connection, ConnectionError from ansible.module_utils.common._collections_compat import Mapping from ansible.module_utils.network.common.config import NetworkConfig, dumps -from ansible.module_utils.six import iteritems, string_types +from ansible.module_utils.six import iteritems, string_types, PY2, PY3 from ansible.module_utils.urls import fetch_url +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +try: + if PY3: + from collections import OrderedDict + else: + from ordereddict import OrderedDict + HAS_ORDEREDDICT = True +except ImportError: + HAS_ORDEREDDICT = False + _DEVICE_CONNECTION = None nxos_provider_spec = { @@ -687,6 +702,359 @@ class HttpApi: return None +class NxosCmdRef: + """NXOS Command Reference utilities. + The NxosCmdRef class takes a yaml-formatted string of nxos module commands + and converts it into dict-formatted database of getters/setters/defaults + and associated common and platform-specific values. The utility methods + add additional data such as existing states, playbook states, and proposed cli. + The utilities also abstract away platform differences such as different + defaults and different command syntax. + + Callers must provide a yaml formatted string that defines each command and + its properties; e.g. BFD global: + --- + _template: # _template holds common settings for all commands + # Enable feature bfd if disabled + feature: bfd + # Common getter syntax for BFD commands + get_command: show run bfd all | incl '^(no )*bfd' + + interval: + kind: dict + getval: bfd interval (?P\\d+) min_rx (?P\\d+) multiplier (?P\\d+) + setval: bfd interval {tx} min_rx {min_rx} multiplier {multiplier} + default: + tx: 50 + min_rx: 50 + multiplier: 3 + N3K: + # Platform overrides + default: + tx: 250 + min_rx: 250 + multiplier: 3 + """ + + def __init__(self, module, cmd_ref_str): + """Initialize cmd_ref from yaml data.""" + self._module = module + self._check_imports() + self._yaml_load(cmd_ref_str) + ref = self._ref + + # Create a list of supported commands based on ref keys + ref['commands'] = sorted([k for k in ref if not k.startswith('_')]) + ref['_proposed'] = [] + ref['_state'] = module.params.get('state', 'present') + self.feature_enable() + self.get_platform_defaults() + self.normalize_defaults() + + def __getitem__(self, key=None): + if key is None: + return self._ref + return self._ref[key] + + def _check_imports(self): + module = self._module + msg = nxosCmdRef_import_check() + if msg: + module.fail_json(msg=msg) + + def _yaml_load(self, cmd_ref_str): + if PY2: + self._ref = yaml.load(cmd_ref_str) + elif PY3: + self._ref = yaml.load(cmd_ref_str, Loader=yaml.FullLoader) + + def feature_enable(self): + """Add 'feature ' to _proposed if ref includes a 'feature' key. """ + ref = self._ref + feature = ref['_template'].get('feature') + if feature: + show_cmd = "show run | incl 'feature {0}'".format(feature) + output = self.execute_show_command(show_cmd, 'text') + if not output or 'CLI command error' in output: + msg = "** 'feature {0}' is not enabled. Module will auto-enable feature {0} ** ".format(feature) + self._module.warn(msg) + ref['_proposed'].append('feature {0}'.format(feature)) + ref['_cli_is_feature_disabled'] = ref['_proposed'] + + def get_platform_shortname(self): + """Query device for platform type, normalize to a shortname/nickname. + Returns platform shortname (e.g. 'N3K-3058P' returns 'N3K') or None. + """ + # TBD: add this method logic to get_capabilities() after those methods + # are made consistent across transports + platform_info = self.execute_show_command('show inventory', 'json') + if not platform_info or not isinstance(platform_info, dict): + return None + inventory_table = platform_info['TABLE_inv']['ROW_inv'] + for info in inventory_table: + if 'Chassis' in info['name']: + network_os_platform = info['productid'] + break + else: + return None + + # Supported Platforms: N3K,N5K,N6K,N7K,N9K,N3K-F,N9K-F + m = re.match('(?PN[35679][K57])-(?PC35)*', network_os_platform) + if not m: + return None + shortname = m.group('short') + + # Normalize + if m.groupdict().get('N35'): + shortname = 'N35' + elif re.match('N77', shortname): + shortname = 'N7K' + elif re.match(r'N3K|N9K', shortname): + for info in inventory_table: + if '-R' in info['productid']: + # Fretta Platform + shortname += '-F' + break + return shortname + + def get_platform_defaults(self): + """Update ref with platform specific defaults""" + plat = self.get_platform_shortname() + if not plat: + return + + ref = self._ref + ref['_platform_shortname'] = plat + # Remove excluded commands (no platform support for command) + for k in ref['commands']: + if plat in ref[k].get('_exclude', ''): + ref['commands'].remove(k) + + # Update platform-specific settings for each item in ref + plat_spec_cmds = [k for k in ref['commands'] if plat in ref[k]] + for k in plat_spec_cmds: + for plat_key in ref[k][plat]: + ref[k][plat_key] = ref[k][plat][plat_key] + + def normalize_defaults(self): + """Update ref defaults with normalized data""" + ref = self._ref + for k in ref['commands']: + if 'default' in ref[k] and ref[k]['default']: + kind = ref[k]['kind'] + if 'int' == kind: + ref[k]['default'] = int(ref[k]['default']) + elif 'list' == kind: + ref[k]['default'] = [str(i) for i in ref[k]['default']] + elif 'dict' == kind: + for key, v in ref[k]['default'].items(): + if v: + v = str(v) + ref[k]['default'][key] = v + + def execute_show_command(self, command, format): + """Generic show command helper. + Warning: 'CLI command error' exceptions are caught, must be handled by caller. + Return device output as a newline-separated string or None. + """ + cmds = [{ + 'command': command, + 'output': format, + }] + output = None + try: + output = run_commands(self._module, cmds) + if output: + output = output[0] + except ConnectionError as exc: + if 'CLI command error' in repr(exc): + # CLI may be feature disabled + output = repr(exc) + else: + raise + return output + + def pattern_match_existing(self, output, k): + """Pattern matching helper for `get_existing`. + `k` is the command name string. Use the pattern from cmd_ref to + find a matching string in the output. + Return regex match object or None. + """ + ref = self._ref + pattern = re.compile(ref[k]['getval']) + match_lines = [re.search(pattern, line) for line in output] + if 'dict' == ref[k]['kind']: + match = [m for m in match_lines if m] + if not match: + return None + match = match[0] + + else: + match = [m.groups() for m in match_lines if m] + if not match: + return None + if len(match) > 1: + # TBD: Add support for multiple instances + raise ValueError("get_existing: multiple match instances are not currently supported") + match = list(match[0]) # tuple to list + + # Handle config strings that nvgen with the 'no' prefix. + # Example match behavior: + # When pattern is: '(no )*foo *(\S+)*$' AND + # When output is: 'no foo' -> match: ['no ', None] + # When output is: 'foo 50' -> match: [None, '50'] + if None is match[0]: + match.pop(0) + elif 'no' in match[0]: + match.pop(0) + if not match: + return None + + return match + + def get_existing(self): + """Update ref with existing command states from the device. + Store these states in each command's 'existing' key. + """ + ref = self._ref + if ref.get('_cli_is_feature_disabled'): + return + show_cmd = ref['_template']['get_command'] + output = self.execute_show_command(show_cmd, 'text') or [] + if not output: + return + + # Walk each cmd in ref, use cmd pattern to discover existing cmds + output = output.split('\n') + for k in ref['commands']: + match = self.pattern_match_existing(output, k) + if not match: + continue + kind = ref[k]['kind'] + if 'int' == kind: + ref[k]['existing'] = int(match[0]) + elif 'list' == kind: + ref[k]['existing'] = [str(i) for i in match] + elif 'dict' == kind: + # The getval pattern should contain regex named group keys that + # match up with the setval named placeholder keys; e.g. + # getval: my-cmd (?P\d+) bar (?P\d+) + # setval: my-cmd {foo} bar {baz} + ref[k]['existing'] = {} + for key in match.groupdict().keys(): + ref[k]['existing'][key] = str(match.group(key)) + elif 'str' == kind: + ref[k]['existing'] = match[0] + else: + raise ValueError("get_existing: unknown 'kind' value specified for key '{0}'".format(k)) + + def get_playvals(self): + """Update ref with values from the playbook. + Store these values in each command's 'playval' key. + """ + ref = self._ref + module = self._module + for k in ref.keys(): + if k in module.params and module.params[k] is not None: + playval = module.params[k] + # Normalize each value + if 'int' == ref[k]['kind']: + playval = int(playval) + elif 'list' == ref[k]['kind']: + playval = [str(i) for i in playval] + elif 'dict' == ref[k]['kind']: + for key, v in playval.items(): + playval[key] = str(v) + ref[k]['playval'] = playval + + def get_proposed(self): + """Compare playbook values against existing states and create a list + of proposed commands. + Return a list of raw cli command strings. + """ + ref = self._ref + # '_proposed' may be empty list or contain initializations; e.g. ['feature foo'] + proposed = ref['_proposed'] + # Create a list of commands that have playbook values + play_keys = [k for k in ref['commands'] if 'playval' in ref[k]] + + # Compare against current state + for k in play_keys: + playval = ref[k]['playval'] + existing = ref[k].get('existing', ref[k]['default']) + if playval == existing and ref['_state'] == 'present': + continue + if isinstance(existing, dict) and all(x is None for x in existing.values()): + existing = None + if existing is None and ref['_state'] == 'absent': + continue + cmd = None + kind = ref[k]['kind'] + if 'int' == kind: + cmd = ref[k]['setval'].format(playval) + elif 'list' == kind: + cmd = ref[k]['setval'].format(*(playval)) + elif 'dict' == kind: + # The setval pattern should contain placeholder keys that + # match up with the getval regex named group keys; e.g. + # getval: my-cmd (?P\d+) bar (?P\d+) + # setval: my-cmd {foo} bar {baz} + cmd = ref[k]['setval'].format(**playval) + elif 'str' == kind: + if 'deleted' in playval: + if existing: + cmd = 'no ' + ref[k]['setval'].format(existing) + else: + cmd = ref[k]['setval'].format(playval) + else: + raise ValueError("get_proposed: unknown 'kind' value specified for key '{0}'".format(k)) + if cmd: + if 'absent' == ref['_state'] and not re.search(r'^no', cmd): + cmd = 'no ' + cmd + # Add processed command to cmd_ref object + ref[k]['setcmd'] = cmd + + # Commands may require parent commands for proper context. + # Global _template context is replaced by parameter context + for k in play_keys: + if ref[k].get('setcmd') is None: + continue + parent_context = ref['_template'].get('context', []) + parent_context = ref[k].get('context', parent_context) + if isinstance(parent_context, list): + for ctx_cmd in parent_context: + if re.search(r'setval::', ctx_cmd): + ctx_cmd = ref[ctx_cmd.split('::')[1]].get('setcmd') + if ctx_cmd is None: + continue + proposed.append(ctx_cmd) + elif isinstance(parent_context, str): + if re.search(r'setval::', parent_context): + parent_context = ref[parent_context.split('::')[1]].get('setcmd') + if parent_context is None: + continue + proposed.append(parent_context) + + proposed.append(ref[k]['setcmd']) + + # Remove duplicate commands from proposed before returning + return OrderedDict.fromkeys(proposed).keys() + + +def nxosCmdRef_import_check(): + """Return import error messages or empty string""" + msg = '' + if PY2: + if not HAS_ORDEREDDICT: + msg += "Mandatory python library 'ordereddict' is not present, try 'pip install ordereddict'\n" + if not HAS_YAML: + msg += "Mandatory python library 'yaml' is not present, try 'pip install yaml'\n" + elif PY3: + if not HAS_YAML: + msg += "Mandatory python library 'PyYAML' is not present, try 'pip install PyYAML'\n" + return msg + + def is_json(cmd): return to_text(cmd).endswith('| json') diff --git a/lib/ansible/modules/network/nxos/nxos_bfd_global.py b/lib/ansible/modules/network/nxos/nxos_bfd_global.py new file mode 100644 index 0000000000..95a8b0845d --- /dev/null +++ b/lib/ansible/modules/network/nxos/nxos_bfd_global.py @@ -0,0 +1,315 @@ +#!/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 . +# + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'network'} + + +DOCUMENTATION = ''' +--- +module: nxos_bfd_global +extends_documentation_fragment: nxos +version_added: "2.9" +short_description: Bidirectional Forwarding Detection (BFD) global-level configuration +description: + - Manages Bidirectional Forwarding Detection (BFD) global-level configuration. +author: + - Chris Van Heuveln (@chrisvanheuveln) +notes: + - Tested against NXOSv 9.2(2) + - 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: + # Top-level commands + 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/IPv6 specific commands + 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 commands + 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 = ''' +- 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.network.nxos.nxos import NxosCmdRef +from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args +from ansible.module_utils.network.nxos.nxos import load_config, run_commands +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.network.common.config import CustomNetworkConfig + +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\\d+) min_rx (?P\\d+) multiplier (?P\\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\\d+) min_rx (?P\\d+) multiplier (?P\\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\\d+) min_rx (?P\\d+) multiplier (?P\\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\\d+) min_rx (?P\\d+) multiplier (?P\\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 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'), + ) + argument_spec.update(nxos_argument_spec) + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) + warnings = list() + check_args(module, warnings) + + cmd_ref = NxosCmdRef(module, BFD_CMD_REF) + cmd_ref.get_existing() + cmd_ref.get_playvals() + 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/test/integration/targets/nxos_bfd_global/defaults/main.yaml b/test/integration/targets/nxos_bfd_global/defaults/main.yaml new file mode 100644 index 0000000000..5f709c5aac --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/defaults/main.yaml @@ -0,0 +1,2 @@ +--- +testcase: "*" diff --git a/test/integration/targets/nxos_bfd_global/meta/main.yml b/test/integration/targets/nxos_bfd_global/meta/main.yml new file mode 100644 index 0000000000..ae741cbdc7 --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - prepare_nxos_tests diff --git a/test/integration/targets/nxos_bfd_global/tasks/cli.yaml b/test/integration/targets/nxos_bfd_global/tasks/cli.yaml new file mode 100644 index 0000000000..9b62eaba65 --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/tasks/cli.yaml @@ -0,0 +1,27 @@ +--- +- name: collect common test cases + find: + paths: "{{ role_path }}/tests/common" + patterns: "{{ testcase }}.yaml" + connection: local + register: test_cases + +- name: collect cli test cases + find: + paths: "{{ role_path }}/tests/cli" + patterns: "{{ testcase }}.yaml" + connection: local + register: cli_cases + +- set_fact: + test_cases: + files: "{{ test_cases.files }} + {{ cli_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=network_cli) + include: "{{ test_case_to_run }} ansible_connection=network_cli connection={{ cli }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_bfd_global/tasks/main.yaml b/test/integration/targets/nxos_bfd_global/tasks/main.yaml new file mode 100644 index 0000000000..4b0f8c64d9 --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- { include: cli.yaml, tags: ['cli'] } +- { include: nxapi.yaml, tags: ['nxapi'] } diff --git a/test/integration/targets/nxos_bfd_global/tasks/nxapi.yaml b/test/integration/targets/nxos_bfd_global/tasks/nxapi.yaml new file mode 100644 index 0000000000..04c99602e6 --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/tasks/nxapi.yaml @@ -0,0 +1,33 @@ +--- +- name: collect common test cases + find: + paths: "{{ role_path }}/tests/common" + patterns: "{{ testcase }}.yaml" + connection: local + register: test_cases + +- name: collect nxapi test cases + find: + paths: "{{ role_path }}/tests/nxapi" + patterns: "{{ testcase }}.yaml" + connection: local + register: nxapi_cases + +- set_fact: + test_cases: + files: "{{ test_cases.files }} + {{ nxapi_cases.files }}" + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test cases (connection=httpapi) + include: "{{ test_case_to_run }} ansible_connection=httpapi connection={{ nxapi }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run + +- name: run test cases (connection=local) + include: "{{ test_case_to_run }} ansible_connection=local connection={{ nxapi }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/targets/nxos_bfd_global/tests/common/sanity.yaml b/test/integration/targets/nxos_bfd_global/tests/common/sanity.yaml new file mode 100644 index 0000000000..d996819267 --- /dev/null +++ b/test/integration/targets/nxos_bfd_global/tests/common/sanity.yaml @@ -0,0 +1,152 @@ +--- +- debug: msg="START connection={{ ansible_connection }} nxos_bfd_global sanity test" +- debug: msg="Using provider={{ connection.transport }}" + when: ansible_connection == "local" + +- name: set facts common + # nd_* vars are "non-default" values + set_fact: + echo: deleted + nd_echo: loopback1 + interval: &def_interval + tx: 50 + min_rx: 50 + multiplier: 3 + nd_interval: + tx: 51 + min_rx: 52 + multiplier: 4 + slow: 2000 + nd_slow: 2001 + +- name: set facts (exclude 5K/6K) + set_fact: + echo_rx: 50 + nd_echo_rx: 51 + ipv4_echo_rx: 50 + nd_ipv4_echo_rx: 54 + ipv4_interval: *def_interval + nd_ipv4_interval: &nd_afi_interval + tx: 54 + min_rx: 56 + multiplier: 8 + ipv4_slow: 2000 + nd_ipv4_slow: 2044 + when: platform is not search('N5K|N6K') + +- name: set facts (exclude 35/5K/6K) + set_fact: + ipv6_echo_rx: 50 + nd_ipv6_echo_rx: 56 + ipv6_interval: *def_interval + nd_ipv6_interval: *nd_afi_interval + ipv6_slow: 2000 + nd_ipv6_slow: 2046 + when: platform is not search('N35|N5K|N6K') + +- name: set facts (exclude 5K/6K/7K) + set_fact: + startup: 5 + nd_startup: 6 + when: platform is not search('N35|N5K|N6K|N7K') + +- name: set facts 3k defaults (resets some values above) + set_fact: + echo_rx: 250 + interval: &n3k_def_interval + tx: 250 + min_rx: 250 + multiplier: 3 + ipv4_echo_rx: 250 + ipv6_echo_rx: 250 + ipv4_interval: *n3k_def_interval + ipv6_interval: *n3k_def_interval + ipv4_slow: 2000 + ipv6_slow: 2000 + when: platform is search('N3K') + +- name: set facts fabricpath + set_fact: + fab_interval: *def_interval + nd_fab_interval: + tx: 57 + min_rx: 57 + multiplier: 7 + fab_slow_timer: 2000 + nd_fab_slow_timer: 2007 + fab_vlan: 1 + nd_fab_vlan: 47 + when: platform is not search('N35|N3K|N9K') + +- name: Setup + nxos_feature: &setup_teardown + feature: bfd + provider: "{{ connection }}" + state: disabled + ignore_errors: yes + +- block: + - name: BFD non defaults + nxos_bfd_global: &bfd_non_def + echo_interface: "{{ nd_echo }}" + echo_rx_interval: "{{ nd_echo_rx | default(omit) }}" + interval: "{{ nd_interval }}" + slow_timer: "{{ nd_slow }}" + startup_timer: "{{ nd_startup | default(omit) }}" + ipv4_echo_rx_interval: "{{ nd_ipv4_echo_rx | default(omit) }}" + ipv6_echo_rx_interval: "{{ nd_ipv6_echo_rx | default(omit) }}" + ipv4_interval: "{{ nd_ipv4_interval | default(omit) }}" + ipv6_interval: "{{ nd_ipv6_interval | default(omit) }}" + ipv4_slow_timer: "{{ nd_ipv4_slow | default(omit) }}" + ipv6_slow_timer: "{{ nd_ipv6_slow | default(omit) }}" + fabricpath_interval: "{{ nd_fab_interval | default(omit) }}" + fabricpath_slow_timer: "{{ nd_fab_slow | default(omit) }}" + fabricpath_vlan: "{{ nd_fab_vlan | default(omit) }}" + provider: "{{ connection }}" + register: result + + - assert: &true + that: + - "result.changed == true" + + - name: bfd_non_def idempotence + nxos_bfd_global: *bfd_non_def + register: result + + - assert: &false + that: + - "result.changed == false" + + - name: BFD defaults + nxos_bfd_global: &bfd_def + echo_interface: "{{ echo }}" + echo_rx_interval: "{{ echo_rx | default(omit) }}" + interval: "{{ interval }}" + slow_timer: "{{ slow }}" + startup_timer: "{{ startup | default(omit) }}" + ipv4_echo_rx_interval: "{{ ipv4_echo_rx | default(omit) }}" + ipv6_echo_rx_interval: "{{ ipv6_echo_rx | default(omit) }}" + ipv4_interval: "{{ ipv4_interval | default(omit) }}" + ipv6_interval: "{{ ipv6_interval | default(omit) }}" + ipv4_slow_timer: "{{ ipv4_slow | default(omit) }}" + ipv6_slow_timer: "{{ ipv6_slow | default(omit) }}" + fabricpath_interval: "{{ fab_interval | default(omit) }}" + fabricpath_slow_timer: "{{ fab_slow | default(omit) }}" + fabricpath_vlan: "{{ fab_vlan | default(omit) }}" + provider: "{{ connection }}" + register: result + + - assert: *true + + - name: bfd_def idempotence + nxos_bfd_global: *bfd_def + register: result + + - assert: *false + + always: + - name: Teardown + nxos_feature: *setup_teardown + ignore_errors: yes + +- debug: msg="END connection={{ ansible_connection }} nxos_bfd_global sanity test" diff --git a/test/integration/targets/prepare_nxos_tests/tasks/main.yml b/test/integration/targets/prepare_nxos_tests/tasks/main.yml index 5d7ab5ed8d..ff91587187 100644 --- a/test/integration/targets/prepare_nxos_tests/tasks/main.yml +++ b/test/integration/targets/prepare_nxos_tests/tasks/main.yml @@ -43,7 +43,7 @@ # Get image version information for this device - name: "Gather image version info" nxos_command: - commands: ['sh version | json'] + commands: ['show version | json'] connection: network_cli register: nxos_version_output @@ -58,7 +58,7 @@ # - name: "Gather platform info" nxos_command: - commands: ['sh inventory | json'] + commands: ['show inventory | json'] connection: network_cli register: nxos_inventory_output diff --git a/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N7K.cfg b/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N7K.cfg new file mode 100644 index 0000000000..efe9626869 --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N7K.cfg @@ -0,0 +1,16 @@ +feature bfd + +bfd echo-interface loopback2 +bfd echo-rx-interval 56 +bfd interval 51 min_rx 52 multiplier 4 +bfd slow-timer 2001 +bfd startup-timer 6 +bfd ipv4 echo-rx-interval 54 +bfd ipv4 interval 54 min_rx 54 multiplier 4 +bfd ipv4 slow-timer 2004 +bfd ipv6 echo-rx-interval 56 +bfd ipv6 interval 56 min_rx 56 multiplier 6 +bfd ipv6 slow-timer 2006 +bfd fabricpath slow-timer 2008 +bfd fabricpath interval 58 min_rx 58 multiplier 8 +bfd fabricpath vlan 2 diff --git a/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N9K.cfg b/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N9K.cfg new file mode 100644 index 0000000000..ab1d2d4a51 --- /dev/null +++ b/test/units/modules/network/nxos/fixtures/nxos_bfd_global/N9K.cfg @@ -0,0 +1,13 @@ +feature bfd + +bfd echo-interface loopback2 +bfd echo-rx-interval 56 +bfd interval 51 min_rx 52 multiplier 4 +bfd slow-timer 2001 +bfd startup-timer 6 +bfd ipv4 echo-rx-interval 54 +bfd ipv4 interval 54 min_rx 54 multiplier 4 +bfd ipv4 slow-timer 2004 +bfd ipv6 echo-rx-interval 56 +bfd ipv6 interval 56 min_rx 56 multiplier 6 +bfd ipv6 slow-timer 2006 diff --git a/test/units/modules/network/nxos/test_nxos_bfd_global.py b/test/units/modules/network/nxos/test_nxos_bfd_global.py new file mode 100644 index 0000000000..be54e5cbe7 --- /dev/null +++ b/test/units/modules/network/nxos/test_nxos_bfd_global.py @@ -0,0 +1,251 @@ +# (c) 2019 Red Hat Inc. +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from units.compat.mock import patch +from ansible.modules.network.nxos import nxos_bfd_global +from ansible.module_utils.network.nxos.nxos import NxosCmdRef +from .nxos_module import TestNxosModule, load_fixture, set_module_args + +# TBD: These imports / import checks are only needed as a workaround for +# shippable, which fails this test due to import yaml & import ordereddict. +import pytest +from ansible.module_utils.network.nxos.nxos import nxosCmdRef_import_check +msg = nxosCmdRef_import_check() +@pytest.mark.skipif(len(msg), reason=msg) +class TestNxosBfdGlobalModule(TestNxosModule): + + module = nxos_bfd_global + + def setUp(self): + super(TestNxosBfdGlobalModule, self).setUp() + + self.mock_load_config = patch('ansible.modules.network.nxos.nxos_bfd_global.load_config') + self.load_config = self.mock_load_config.start() + + self.mock_execute_show_command = patch('ansible.module_utils.network.nxos.nxos.NxosCmdRef.execute_show_command') + self.execute_show_command = self.mock_execute_show_command.start() + + self.mock_get_platform_shortname = patch('ansible.module_utils.network.nxos.nxos.NxosCmdRef.get_platform_shortname') + self.get_platform_shortname = self.mock_get_platform_shortname.start() + + def tearDown(self): + super(TestNxosBfdGlobalModule, self).tearDown() + self.mock_load_config.stop() + self.execute_show_command.stop() + self.get_platform_shortname.stop() + + def load_fixtures(self, commands=None, device=''): + self.load_config.return_value = None + + def test_bfd_defaults_n9k(self): + # feature bfd is enabled, no non-defaults are set. + self.execute_show_command.return_value = "feature bfd" + self.get_platform_shortname.return_value = 'N9K' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=50, + interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + slow_timer=2000, + startup_timer=5, + ipv4_echo_rx_interval=50, + ipv4_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + ipv4_slow_timer=2000, + ipv6_echo_rx_interval=50, + ipv6_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + ipv6_slow_timer=2000 + )) + self.execute_module(changed=False) + + def test_bfd_defaults_n3k(self): + # feature bfd is enabled, no non-defaults are set. + self.execute_show_command.return_value = "feature bfd" + self.get_platform_shortname.return_value = 'N3K' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=250, + interval={'tx': 250, 'min_rx': 250, 'multiplier': 3}, + slow_timer=2000, + startup_timer=5, + ipv4_echo_rx_interval=250, + ipv4_interval={'tx': 250, 'min_rx': 250, 'multiplier': 3}, + ipv4_slow_timer=2000, + ipv6_echo_rx_interval=250, + ipv6_interval={'tx': 250, 'min_rx': 250, 'multiplier': 3}, + ipv6_slow_timer=2000 + )) + self.execute_module(changed=False) + + def test_bfd_defaults_n35(self): + # feature bfd is enabled, no non-defaults are set. + self.execute_show_command.return_value = "feature bfd" + self.get_platform_shortname.return_value = 'N35' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=50, + interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + slow_timer=2000, + startup_timer=5, + ipv4_echo_rx_interval=50, + ipv4_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + ipv4_slow_timer=2000, + )) + self.execute_module(changed=False) + + def test_bfd_defaults_n6k(self): + # feature bfd is enabled, no non-defaults are set. + self.execute_show_command.return_value = "feature bfd" + self.get_platform_shortname.return_value = 'N6K' + set_module_args(dict( + echo_interface='deleted', + interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + slow_timer=2000, + fabricpath_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + fabricpath_slow_timer=2000, + fabricpath_vlan=1 + )) + self.execute_module(changed=False) + + def test_bfd_defaults_n7k(self): + # feature bfd is enabled, no non-defaults are set. + self.execute_show_command.return_value = "feature bfd" + self.get_platform_shortname.return_value = 'N7K' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=50, + interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + slow_timer=2000, + ipv4_echo_rx_interval=50, + ipv4_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + ipv4_slow_timer=2000, + ipv6_echo_rx_interval=50, + ipv6_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + ipv6_slow_timer=2000, + fabricpath_interval={'tx': 50, 'min_rx': 50, 'multiplier': 3}, + fabricpath_slow_timer=2000, + fabricpath_vlan=1 + )) + self.execute_module(changed=False) + + def test_bfd_existing_n9k(self): + module_name = self.module.__name__.rsplit('.', 1)[1] + self.execute_show_command.return_value = load_fixture(module_name, 'N9K.cfg') + self.get_platform_shortname.return_value = 'N9K' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=51, + interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + slow_timer=2000, + startup_timer=5, + ipv4_echo_rx_interval=50, + ipv4_interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + ipv4_slow_timer=2000, + ipv6_echo_rx_interval=50, + ipv6_interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + ipv6_slow_timer=2000 + )) + self.execute_module(changed=True, commands=[ + 'no bfd echo-interface loopback2', + 'bfd echo-rx-interval 51', + 'bfd interval 51 min_rx 51 multiplier 3', + 'bfd slow-timer 2000', + 'bfd startup-timer 5', + 'bfd ipv4 echo-rx-interval 50', + 'bfd ipv4 interval 51 min_rx 51 multiplier 3', + 'bfd ipv4 slow-timer 2000', + 'bfd ipv6 echo-rx-interval 50', + 'bfd ipv6 interval 51 min_rx 51 multiplier 3', + 'bfd ipv6 slow-timer 2000', + ]) + + def test_bfd_idempotence_n9k(self): + module_name = self.module.__name__.rsplit('.', 1)[1] + self.execute_show_command.return_value = load_fixture(module_name, 'N9K.cfg') + self.get_platform_shortname.return_value = 'N9K' + set_module_args(dict( + echo_interface='loopback2', + echo_rx_interval=56, + interval={'tx': 51, 'min_rx': 52, 'multiplier': 4}, + slow_timer=2001, + startup_timer=6, + ipv4_echo_rx_interval=54, + ipv4_interval={'tx': 54, 'min_rx': 54, 'multiplier': 4}, + ipv4_slow_timer=2004, + ipv6_echo_rx_interval=56, + ipv6_interval={'tx': 56, 'min_rx': 56, 'multiplier': 6}, + ipv6_slow_timer=2006 + )) + self.execute_module(changed=False) + + def test_bfd_existing_n7k(self): + module_name = self.module.__name__.rsplit('.', 1)[1] + self.execute_show_command.return_value = load_fixture(module_name, 'N7K.cfg') + self.get_platform_shortname.return_value = 'N7K' + set_module_args(dict( + echo_interface='deleted', + echo_rx_interval=51, + interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + slow_timer=2002, + ipv4_echo_rx_interval=51, + ipv4_interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + ipv4_slow_timer=2002, + ipv6_echo_rx_interval=51, + ipv6_interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + ipv6_slow_timer=2002, + fabricpath_interval={'tx': 51, 'min_rx': 51, 'multiplier': 3}, + fabricpath_slow_timer=2003, + fabricpath_vlan=3, + )) + self.execute_module(changed=True, commands=[ + 'no bfd echo-interface loopback2', + 'bfd echo-rx-interval 51', + 'bfd interval 51 min_rx 51 multiplier 3', + 'bfd slow-timer 2002', + 'bfd ipv4 echo-rx-interval 51', + 'bfd ipv4 interval 51 min_rx 51 multiplier 3', + 'bfd ipv4 slow-timer 2002', + 'bfd ipv6 echo-rx-interval 51', + 'bfd ipv6 interval 51 min_rx 51 multiplier 3', + 'bfd ipv6 slow-timer 2002', + 'bfd fabricpath interval 51 min_rx 51 multiplier 3', + 'bfd fabricpath slow-timer 2003', + 'bfd fabricpath vlan 3', + ]) + + def test_bfd_idempotence_n7k(self): + module_name = self.module.__name__.rsplit('.', 1)[1] + self.execute_show_command.return_value = load_fixture(module_name, 'N7K.cfg') + self.get_platform_shortname.return_value = 'N7K' + set_module_args(dict( + echo_interface='loopback2', + echo_rx_interval=56, + interval={'tx': 51, 'min_rx': 52, 'multiplier': 4}, + slow_timer=2001, + ipv4_echo_rx_interval=54, + ipv4_interval={'tx': 54, 'min_rx': 54, 'multiplier': 4}, + ipv4_slow_timer=2004, + ipv6_echo_rx_interval=56, + ipv6_interval={'tx': 56, 'min_rx': 56, 'multiplier': 6}, + ipv6_slow_timer=2006, + fabricpath_interval={'tx': 58, 'min_rx': 58, 'multiplier': 8}, + fabricpath_slow_timer=2008, + fabricpath_vlan=2, + )) + self.execute_module(changed=False)