Add CmdRef logic to handle multiples (#57495)

* Add CmdRef logic to handle multiples

* Fix python 2.6 issues

* Fix bug when existing is not a dict object

* Fix python2 vs python3 incompatibilty

* Ignore unnecessary-lambda warning
This commit is contained in:
Mike Wiebe 2019-06-19 11:45:32 -04:00 committed by Trishna Guha
parent 8f68c8d438
commit 12d656901f

View file

@ -746,6 +746,8 @@ class NxosCmdRef:
# Create a list of supported commands based on ref keys # Create a list of supported commands based on ref keys
ref['commands'] = sorted([k for k in ref if not k.startswith('_')]) ref['commands'] = sorted([k for k in ref if not k.startswith('_')])
ref['_proposed'] = [] ref['_proposed'] = []
ref['_context'] = []
ref['_resource_key'] = None
ref['_state'] = module.params.get('state', 'present') ref['_state'] = module.params.get('state', 'present')
self.feature_enable() self.feature_enable()
self.get_platform_defaults() self.get_platform_defaults()
@ -882,46 +884,83 @@ class NxosCmdRef:
""" """
ref = self._ref ref = self._ref
pattern = re.compile(ref[k]['getval']) pattern = re.compile(ref[k]['getval'])
multiple = 'multiple' in ref[k].keys()
match_lines = [re.search(pattern, line) for line in output] match_lines = [re.search(pattern, line) for line in output]
if 'dict' == ref[k]['kind']: if 'dict' == ref[k]['kind']:
match = [m for m in match_lines if m] match = [m for m in match_lines if m]
if not match: if not match:
return None return None
match = match[0] if len(match) > 1 and not multiple:
raise ValueError("get_existing: multiple matches found for property {0}".format(k))
else: else:
match = [m.groups() for m in match_lines if m] match = [m.groups() for m in match_lines if m]
if not match: if not match:
return None return None
if len(match) > 1: if len(match) > 1 and not multiple:
# TBD: Add support for multiple instances raise ValueError("get_existing: multiple matches found for property {0}".format(k))
raise ValueError("get_existing: multiple match instances are not currently supported") for item in match:
match = list(match[0]) # tuple to list index = match.index(item)
match[index] = list(item) # tuple to list
# Handle config strings that nvgen with the 'no' prefix. # Handle config strings that nvgen with the 'no' prefix.
# Example match behavior: # Example match behavior:
# When pattern is: '(no )*foo *(\S+)*$' AND # When pattern is: '(no )*foo *(\S+)*$' AND
# When output is: 'no foo' -> match: ['no ', None] # When output is: 'no foo' -> match: ['no ', None]
# When output is: 'foo 50' -> match: [None, '50'] # When output is: 'foo 50' -> match: [None, '50']
if None is match[0]: if None is match[index][0]:
match.pop(0) match[index].pop(0)
elif 'no' in match[0]: elif 'no' in match[index][0]:
match.pop(0) match[index].pop(0)
if not match: if not match:
return None return None
return match return match
def set_context(self, context=None):
"""Update ref with command context.
"""
if context is None:
context = []
ref = self._ref
# Process any additional context that this propoerty might require.
# 1) Global context from NxosCmdRef _template.
# 2) Context passed in using context arg.
ref['_context'] = ref['_template'].get('context', [])
for cmd in context:
ref['_context'].append(cmd)
# Last key in context is the resource key
ref['_resource_key'] = context[-1] if context else ref['_resource_key']
def get_existing(self): def get_existing(self):
"""Update ref with existing command states from the device. """Update ref with existing command states from the device.
Store these states in each command's 'existing' key. Store these states in each command's 'existing' key.
""" """
ref = self._ref ref = self._ref
if ref.get('_cli_is_feature_disabled'): if ref.get('_cli_is_feature_disabled'):
# Add context to proposed if state is present
if 'present' in ref['_state']:
[ref['_proposed'].append(ctx) for ctx in ref['_context']]
return return
show_cmd = ref['_template']['get_command'] show_cmd = ref['_template']['get_command']
# Add additional command context if needed.
for filter in ref['_context']:
show_cmd = show_cmd + " | section '{0}'".format(filter)
output = self.execute_show_command(show_cmd, 'text') or [] output = self.execute_show_command(show_cmd, 'text') or []
if not output: if not output:
# Add context to proposed if state is present
if 'present' in ref['_state']:
[ref['_proposed'].append(ctx) for ctx in ref['_context']]
return
# We need to remove the last item in context for state absent case.
if 'absent' in ref['_state'] and ref['_context']:
if ref['_resource_key'] and ref['_resource_key'] == ref['_context'][-1]:
if ref['_context'][-1] in output:
ref['_context'][-1] = 'no ' + ref['_context'][-1]
else:
del ref['_context'][-1]
return return
# Walk each cmd in ref, use cmd pattern to discover existing cmds # Walk each cmd in ref, use cmd pattern to discover existing cmds
@ -930,21 +969,24 @@ class NxosCmdRef:
match = self.pattern_match_existing(output, k) match = self.pattern_match_existing(output, k)
if not match: if not match:
continue continue
ref[k]['existing'] = {}
for item in match:
index = match.index(item)
kind = ref[k]['kind'] kind = ref[k]['kind']
if 'int' == kind: if 'int' == kind:
ref[k]['existing'] = int(match[0]) ref[k]['existing'][index] = int(item[0])
elif 'list' == kind: elif 'list' == kind:
ref[k]['existing'] = [str(i) for i in match] ref[k]['existing'][index] = [str(i) for i in item[0]]
elif 'dict' == kind: elif 'dict' == kind:
# The getval pattern should contain regex named group keys that # The getval pattern should contain regex named group keys that
# match up with the setval named placeholder keys; e.g. # match up with the setval named placeholder keys; e.g.
# getval: my-cmd (?P<foo>\d+) bar (?P<baz>\d+) # getval: my-cmd (?P<foo>\d+) bar (?P<baz>\d+)
# setval: my-cmd {foo} bar {baz} # setval: my-cmd {foo} bar {baz}
ref[k]['existing'] = {} ref[k]['existing'][index] = {}
for key in match.groupdict().keys(): for key in item.groupdict().keys():
ref[k]['existing'][key] = str(match.group(key)) ref[k]['existing'][index][key] = str(item.group(key))
elif 'str' == kind: elif 'str' == kind:
ref[k]['existing'] = match[0] ref[k]['existing'][index] = item[0]
else: else:
raise ValueError("get_existing: unknown 'kind' value specified for key '{0}'".format(k)) raise ValueError("get_existing: unknown 'kind' value specified for key '{0}'".format(k))
@ -967,27 +1009,12 @@ class NxosCmdRef:
playval[key] = str(v) playval[key] = str(v)
ref[k]['playval'] = playval ref[k]['playval'] = playval
def get_proposed(self): def build_cmd_set(self, playval, existing, k):
"""Compare playbook values against existing states and create a list """Helper function to create list of commands to configure device
of proposed commands. Return a list of commands
Return a list of raw cli command strings.
""" """
ref = self._ref ref = self._ref
# '_proposed' may be empty list or contain initializations; e.g. ['feature foo']
proposed = ref['_proposed'] 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 cmd = None
kind = ref[k]['kind'] kind = ref[k]['kind']
if 'int' == kind: if 'int' == kind:
@ -1011,34 +1038,77 @@ class NxosCmdRef:
if cmd: if cmd:
if 'absent' == ref['_state'] and not re.search(r'^no', cmd): if 'absent' == ref['_state'] and not re.search(r'^no', cmd):
cmd = 'no ' + cmd cmd = 'no ' + cmd
# Add processed command to cmd_ref object
ref[k]['setcmd'] = cmd
# Commands may require parent commands for proper context. # Commands may require parent commands for proper context.
# Global _template context is replaced by parameter context # Global _template context is replaced by parameter context
[proposed.append(ctx) for ctx in ref['_context']]
[proposed.append(ctx) for ctx in ref[k].get('context', [])]
proposed.append(cmd)
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']
if ref['_context'] and ref['_context'][-1].startswith('no'):
[proposed.append(ctx) for ctx in ref['_context']]
return proposed
# Create a list of commands that have playbook values
play_keys = [k for k in ref['commands'] if 'playval' in ref[k]]
def compare(playval, existing):
if 'present' in ref['_state']:
if existing is None:
return False
elif playval == existing:
return True
elif isinstance(existing, dict) and playval in existing.values():
return True
if 'absent' in ref['_state']:
if isinstance(existing, dict) and all(x is None for x in existing.values()):
existing = None
if existing is None or playval not in existing.values():
return True
return False
# Compare against current state
for k in play_keys: for k in play_keys:
if ref[k].get('setcmd') is None: playval = ref[k]['playval']
continue existing = ref[k].get('existing', ref[k]['default'])
parent_context = ref['_template'].get('context', []) multiple = 'multiple' in ref[k].keys()
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']) # Multiple Instances:
if isinstance(existing, dict) and multiple:
item_found = False
for dkey, dvalue in existing.items():
if isinstance(dvalue, dict):
# Remove values set to string 'None' from dvalue
dvalue = dict((k, v) for k, v in dvalue.items() if v != 'None')
if compare(playval, dvalue):
item_found = True
if item_found:
continue
# Single Instance:
else:
if compare(playval, existing):
continue
# Remove duplicate commands from proposed before returning # Multiple Instances:
return OrderedDict.fromkeys(proposed).keys() if isinstance(existing, dict):
for dkey, dvalue in existing.items():
self.build_cmd_set(playval, dvalue, k)
# Single Instance:
else:
self.build_cmd_set(playval, existing, k)
# Remove any duplicate commands before returning.
# pylint: disable=unnecessary-lambda
return sorted(set(proposed), key=lambda x: proposed.index(x))
def nxosCmdRef_import_check(): def nxosCmdRef_import_check():