nxos_bfd_global / NxosCmdRef initial commit (#56317)

* nxos_bfd_global: initial commit

This is an initial POC with just a few commands included.  The code has been written somewhat generically so that it can act as a best practices template for re-use in future modules. The implementation follows the yaml cmd_ref style to define each command's getter/setter/type/default. It supports platform-specific defaults.

The basic logic is to collect all relevant data in a `cmd_ref` dict and pass that around to various methods.

In the BFD case the devices don't provide JSON output so we have to screen-scrape with show runs.

BFD does not support present/absent states so there is no state param.

BFD has three different property types to handle. We can add add'l types as needed:

- int
- int_list (list of ints)
- str (needs support for 'no' keyword)

* Use get_capabilities to find platform type

* PR comment fixes, round 1

* Minor cleanups

* nxos_bfd_global: create NxosCmdRef in module_utils

This commit just takes the latest bfd global code and moves the bulk
of the code into new `class NxosCmdRef` in `module_utils/nxos/nxos.py`.

The only remaining code in `nxos_bfd_global.py` are the calls from `main()`.

* Add remaining command properties and documentation

* update argument_spec

* Add check for _exclude; add sanity test

* Add targets files for bfd

* Context and state absent updates

* Add dict support to cmd_ref

* Changed remaining list commands to dict usage

* Add idempotence check for dict

* Fix existing overwrite bug

* Move pattern matching logic into its own method

* add support for 'command: absent'

* Add `get_platform_shortname`; update BFD platform-specific settings

* /absent/deleted/

* /sh/show/ in prepare_nxos_tests

* add dict check to get_platform_shortname

* Add normalize_defaults()

* UTs for bfd_global

* support yaml for both py2/py3

* update cmd_ref doc header

* Fix python2.6 incompatibility with dict comprehensions

* Fix bfd_global doc header (yaml syntax fail)

* more shippable fixes

* yet more shippable fixes

* shippable: remove r' ' wrappers

* docfix - remove ':'

* escape regex ctl chars in yaml table

* remove extra blank lines

* Fix str(None) issue

* Command context updates

* import PY2,PY3 instead of import sys

* fix ordereddict import & parent_context

* try/except for yaml import

* fix import issue for ordereddict

* remove epdb

* nxosCmdRef_import_check() workaround for shippable

* fix PEP ws errors
This commit is contained in:
Chris Van Heuveln 2019-06-06 06:22:55 -04:00 committed by Trishna Guha
parent f65ac2cf23
commit 7aa0d26fda
12 changed files with 1185 additions and 3 deletions

View file

@ -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<tx>\\d+) min_rx (?P<min_rx>\\d+) multiplier (?P<multiplier>\\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 <foo>' 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('(?P<short>N[35679][K57])-(?P<N35>C35)*', 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<foo>\d+) bar (?P<baz>\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<foo>\d+) bar (?P<baz>\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')

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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<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 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()

View file

@ -0,0 +1,2 @@
---
testcase: "*"

View file

@ -0,0 +1,2 @@
dependencies:
- prepare_nxos_tests

View file

@ -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

View file

@ -0,0 +1,3 @@
---
- { include: cli.yaml, tags: ['cli'] }
- { include: nxapi.yaml, tags: ['nxapi'] }

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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)