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
pull/57713/head
Mike Wiebe 5 years ago committed by Trishna Guha
parent 8f68c8d438
commit 12d656901f

@ -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,71 +884,111 @@ 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.
# Example match behavior: # Handle config strings that nvgen with the 'no' prefix.
# When pattern is: '(no )*foo *(\S+)*$' AND # Example match behavior:
# When output is: 'no foo' -> match: ['no ', None] # When pattern is: '(no )*foo *(\S+)*$' AND
# When output is: 'foo 50' -> match: [None, '50'] # When output is: 'no foo' -> match: ['no ', None]
if None is match[0]: # When output is: 'foo 50' -> match: [None, '50']
match.pop(0) if None is match[index][0]:
elif 'no' in match[0]: match[index].pop(0)
match.pop(0) elif 'no' in match[index][0]:
if not match: match[index].pop(0)
return None if not match:
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 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
# Walk each cmd in ref, use cmd pattern to discover existing cmds # Walk each cmd in ref, use cmd pattern to discover existing cmds
output = output.split('\n') output = output.split('\n')
for k in ref['commands']: for k in ref['commands']:
match = self.pattern_match_existing(output, k) match = self.pattern_match_existing(output, k)
if not match: if not match:
continue continue
kind = ref[k]['kind'] ref[k]['existing'] = {}
if 'int' == kind: for item in match:
ref[k]['existing'] = int(match[0]) index = match.index(item)
elif 'list' == kind: kind = ref[k]['kind']
ref[k]['existing'] = [str(i) for i in match] if 'int' == kind:
elif 'dict' == kind: ref[k]['existing'][index] = int(item[0])
# The getval pattern should contain regex named group keys that elif 'list' == kind:
# match up with the setval named placeholder keys; e.g. ref[k]['existing'][index] = [str(i) for i in item[0]]
# getval: my-cmd (?P<foo>\d+) bar (?P<baz>\d+) elif 'dict' == kind:
# setval: my-cmd {foo} bar {baz} # The getval pattern should contain regex named group keys that
ref[k]['existing'] = {} # match up with the setval named placeholder keys; e.g.
for key in match.groupdict().keys(): # getval: my-cmd (?P<foo>\d+) bar (?P<baz>\d+)
ref[k]['existing'][key] = str(match.group(key)) # setval: my-cmd {foo} bar {baz}
elif 'str' == kind: ref[k]['existing'][index] = {}
ref[k]['existing'] = match[0] for key in item.groupdict().keys():
else: ref[k]['existing'][index][key] = str(item.group(key))
raise ValueError("get_existing: unknown 'kind' value specified for key '{0}'".format(k)) elif 'str' == kind:
ref[k]['existing'][index] = item[0]
else:
raise ValueError("get_existing: unknown 'kind' value specified for key '{0}'".format(k))
def get_playvals(self): def get_playvals(self):
"""Update ref with values from the playbook. """Update ref with values from the playbook.
@ -967,6 +1009,41 @@ class NxosCmdRef:
playval[key] = str(v) playval[key] = str(v)
ref[k]['playval'] = playval ref[k]['playval'] = playval
def build_cmd_set(self, playval, existing, k):
"""Helper function to create list of commands to configure device
Return a list of commands
"""
ref = self._ref
proposed = ref['_proposed']
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
# Commands may require parent commands for proper 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): def get_proposed(self):
"""Compare playbook values against existing states and create a list """Compare playbook values against existing states and create a list
of proposed commands. of proposed commands.
@ -975,70 +1052,63 @@ class NxosCmdRef:
ref = self._ref ref = self._ref
# '_proposed' may be empty list or contain initializations; e.g. ['feature foo'] # '_proposed' may be empty list or contain initializations; e.g. ['feature foo']
proposed = ref['_proposed'] 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 # Create a list of commands that have playbook values
play_keys = [k for k in ref['commands'] if 'playval' in ref[k]] 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 # Compare against current state
for k in play_keys: for k in play_keys:
playval = ref[k]['playval'] playval = ref[k]['playval']
existing = ref[k].get('existing', ref[k]['default']) existing = ref[k].get('existing', ref[k]['default'])
if playval == existing and ref['_state'] == 'present': multiple = 'multiple' in ref[k].keys()
continue
if isinstance(existing, dict) and all(x is None for x in existing.values()): # Multiple Instances:
existing = None if isinstance(existing, dict) and multiple:
if existing is None and ref['_state'] == 'absent': item_found = False
continue for dkey, dvalue in existing.items():
cmd = None if isinstance(dvalue, dict):
kind = ref[k]['kind'] # Remove values set to string 'None' from dvalue
if 'int' == kind: dvalue = dict((k, v) for k, v in dvalue.items() if v != 'None')
cmd = ref[k]['setval'].format(playval) if compare(playval, dvalue):
elif 'list' == kind: item_found = True
cmd = ref[k]['setval'].format(*(playval)) if item_found:
elif 'dict' == kind: continue
# The setval pattern should contain placeholder keys that # Single Instance:
# 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: else:
raise ValueError("get_proposed: unknown 'kind' value specified for key '{0}'".format(k)) if compare(playval, existing):
if cmd: continue
if 'absent' == ref['_state'] and not re.search(r'^no', cmd):
cmd = 'no ' + cmd # Multiple Instances:
# Add processed command to cmd_ref object if isinstance(existing, dict):
ref[k]['setcmd'] = cmd for dkey, dvalue in existing.items():
self.build_cmd_set(playval, dvalue, k)
# Commands may require parent commands for proper context. # Single Instance:
# Global _template context is replaced by parameter context else:
for k in play_keys: self.build_cmd_set(playval, existing, k)
if ref[k].get('setcmd') is None:
continue # Remove any duplicate commands before returning.
parent_context = ref['_template'].get('context', []) # pylint: disable=unnecessary-lambda
parent_context = ref[k].get('context', parent_context) return sorted(set(proposed), key=lambda x: proposed.index(x))
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(): def nxosCmdRef_import_check():

Loading…
Cancel
Save