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
ref['commands'] = sorted([k for k in ref if not k.startswith('_')])
ref['_proposed'] = []
ref['_context'] = []
ref['_resource_key'] = None
ref['_state'] = module.params.get('state', 'present')
self.feature_enable()
self.get_platform_defaults()
@ -882,71 +884,111 @@ class NxosCmdRef:
"""
ref = self._ref
pattern = re.compile(ref[k]['getval'])
multiple = 'multiple' in ref[k].keys()
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]
if len(match) > 1 and not multiple:
raise ValueError("get_existing: multiple matches found for property {0}".format(k))
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
if len(match) > 1 and not multiple:
raise ValueError("get_existing: multiple matches found for property {0}".format(k))
for item in match:
index = match.index(item)
match[index] = list(item) # 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
# 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[index][0]:
match[index].pop(0)
elif 'no' in match[index][0]:
match[index].pop(0)
if not match:
return None
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):
"""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'):
# Add context to proposed if state is present
if 'present' in ref['_state']:
[ref['_proposed'].append(ctx) for ctx in ref['_context']]
return
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 []
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
# 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))
ref[k]['existing'] = {}
for item in match:
index = match.index(item)
kind = ref[k]['kind']
if 'int' == kind:
ref[k]['existing'][index] = int(item[0])
elif 'list' == kind:
ref[k]['existing'][index] = [str(i) for i in item[0]]
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'][index] = {}
for key in item.groupdict().keys():
ref[k]['existing'][index][key] = str(item.group(key))
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):
"""Update ref with values from the playbook.
@ -967,6 +1009,41 @@ class NxosCmdRef:
playval[key] = str(v)
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):
"""Compare playbook values against existing states and create a list
of proposed commands.
@ -975,70 +1052,63 @@ class NxosCmdRef:
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:
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)
multiple = 'multiple' in ref[k].keys()
# 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:
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
if compare(playval, existing):
continue
# 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)
# Multiple Instances:
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)
proposed.append(ref[k]['setcmd'])
# Remove duplicate commands from proposed before returning
return OrderedDict.fromkeys(proposed).keys()
# Remove any duplicate commands before returning.
# pylint: disable=unnecessary-lambda
return sorted(set(proposed), key=lambda x: proposed.index(x))
def nxosCmdRef_import_check():