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:
parent
8f68c8d438
commit
12d656901f
1 changed files with 158 additions and 88 deletions
|
@ -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():
|
||||
|
|
Loading…
Reference in a new issue