meraki - Rewrite update requirement check (#48394)

* Rewrite idempotency check
- Check now operates recursively and works on multiple types
- Order of lists matter

* Remove blank line for lint

* Fixed idempotency checks in meraki_ssid
- New sanitize() method for finding keys unique in compared dicts
- Fixed bug in meraki_ssid where SSID specified by number breaks
  - This will require a backport
- Converted ignored_keys from tuple to list

* Made changes required for idempotency

* Add changelog fragment

* Add unidirectional option for testing

* Disable option 1 check

* General fixes for is_update_required testing
- Added commented out debug statements in method
- Fixed ignored_keys modifications

* Remove old commented algorithm
This commit is contained in:
Kevin Breit 2019-05-29 09:18:01 -05:00 committed by Nathaniel Case
parent 57e1063fc7
commit 7864df8cb9
7 changed files with 55 additions and 29 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- "meraki_* - Idempotency check has been rewritten. The new version is more thorough."

View file

@ -70,6 +70,7 @@ class MerakiModule(object):
self.original = None self.original = None
self.proposed = dict() self.proposed = dict()
self.merged = None self.merged = None
self.ignored_keys = ['id', 'organizationId']
# debug output # debug output
self.filter_string = '' self.filter_string = ''
@ -84,7 +85,7 @@ class MerakiModule(object):
'network': '/organizations/{org_id}/networks', 'network': '/organizations/{org_id}/networks',
'admins': '/organizations/{org_id}/admins', 'admins': '/organizations/{org_id}/admins',
'configTemplates': '/organizations/{org_id}/configTemplates', 'configTemplates': '/organizations/{org_id}/configTemplates',
'samlRoles': '/organizations/{org_id}/samlRoles', 'samlymbols': '/organizations/{org_id}/samlRoles',
'ssids': '/networks/{net_id}/ssids', 'ssids': '/networks/{net_id}/ssids',
'groupPolicies': '/networks/{net_id}/groupPolicies', 'groupPolicies': '/networks/{net_id}/groupPolicies',
'staticRoutes': '/networks/{net_id}/staticRoutes', 'staticRoutes': '/networks/{net_id}/staticRoutes',
@ -128,30 +129,48 @@ class MerakiModule(object):
else: else:
self.params['protocol'] = 'http' self.params['protocol'] = 'http'
def is_update_required(self, original, proposed, optional_ignore=None): def sanitize(self, original, proposed):
"""Compare original and proposed data to see if an update is needed.""" """Determine which keys are unique to original"""
is_changed = False keys = []
ignored_keys = ('id', 'organizationId') for k, v in original.items():
if not optional_ignore:
optional_ignore = ('')
# for k, v in original.items():
# try:
# if k not in ignored_keys and k not in optional_ignore:
# if v != proposed[k]:
# is_changed = True
# except KeyError:
# if v != '':
# is_changed = True
for k, v in proposed.items():
try: try:
if k not in ignored_keys and k not in optional_ignore: if proposed[k] and k not in self.ignored_keys:
if v != original[k]: pass
is_changed = True
except KeyError: except KeyError:
if v != '': keys.append(k)
is_changed = True return keys
return is_changed
def is_update_required(self, original, proposed, optional_ignore=None):
''' Compare two data-structures '''
self.ignored_keys.append('net_id')
if optional_ignore is not None:
self.ignored_keys = self.ignored_keys + optional_ignore
if type(original) != type(proposed):
# self.fail_json(msg="Types don't match")
return True
if isinstance(original, list):
if len(original) != len(proposed):
# self.fail_json(msg="Length of lists don't match")
return True
for a, b in zip(original, proposed):
if self.is_update_required(a, b):
# self.fail_json(msg="List doesn't match", a=a, b=b)
return True
elif isinstance(original, dict):
for k, v in proposed.items():
if k not in self.ignored_keys:
if k in original:
if self.is_update_required(original[k], proposed[k]):
return True
else:
# self.fail_json(msg="Key not in original", k=k)
return True
else:
if original != proposed:
# self.fail_json(msg="Fallback", original=original, proposed=proposed)
return True
return False
def get_orgs(self): def get_orgs(self):
"""Downloads all organizations for a user.""" """Downloads all organizations for a user."""

View file

@ -331,9 +331,12 @@ def main():
path = meraki.construct_path('get_all', net_id=net_id) path = meraki.construct_path('get_all', net_id=net_id)
devices = meraki.request(path, method='GET') devices = meraki.request(path, method='GET')
for unit in devices: for unit in devices:
try:
if unit['name'] == meraki.params['hostname']: if unit['name'] == meraki.params['hostname']:
device.append(unit) device.append(unit)
meraki.result['data'] = device meraki.result['data'] = device
except KeyError:
pass
elif meraki.params['model']: elif meraki.params['model']:
path = meraki.construct_path('get_all', net_id=net_id) path = meraki.construct_path('get_all', net_id=net_id)
devices = meraki.request(path, method='GET') devices = meraki.request(path, method='GET')
@ -372,6 +375,7 @@ def main():
query_path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial'] query_path = meraki.construct_path('get_device', net_id=net_id) + meraki.params['serial']
device_data = meraki.request(query_path, method='GET') device_data = meraki.request(query_path, method='GET')
ignore_keys = ['lanIp', 'serial', 'mac', 'model', 'networkId', 'moveMapMarker', 'wan1Ip', 'wan2Ip'] ignore_keys = ['lanIp', 'serial', 'mac', 'model', 'networkId', 'moveMapMarker', 'wan1Ip', 'wan2Ip']
# meraki.fail_json(msg="Compare", original=device_data, payload=payload, ignore=ignore_keys)
if meraki.is_update_required(device_data, payload, optional_ignore=ignore_keys): if meraki.is_update_required(device_data, payload, optional_ignore=ignore_keys):
path = meraki.construct_path('update', net_id=net_id) + meraki.params['serial'] path = meraki.construct_path('update', net_id=net_id) + meraki.params['serial']
updated_device = [] updated_device = []

View file

@ -201,7 +201,7 @@ def set_snmp(meraki, org_id):
full_compare['v2cEnabled'] = False full_compare['v2cEnabled'] = False
path = meraki.construct_path('create', org_id=org_id) path = meraki.construct_path('create', org_id=org_id)
snmp = get_snmp(meraki, org_id) snmp = get_snmp(meraki, org_id)
ignored_parameters = ('v3AuthPass', 'v3PrivPass', 'hostname', 'port', 'v2CommunityString', 'v3User') ignored_parameters = ['v3AuthPass', 'v3PrivPass', 'hostname', 'port', 'v2CommunityString', 'v3User']
if meraki.is_update_required(snmp, full_compare, optional_ignore=ignored_parameters): if meraki.is_update_required(snmp, full_compare, optional_ignore=ignored_parameters):
r = meraki.request(path, r = meraki.request(path,
method='PUT', method='PUT',

View file

@ -377,7 +377,7 @@ def main():
original = meraki.request(query_path, method='GET') original = meraki.request(query_path, method='GET')
if meraki.params['type'] == 'trunk': if meraki.params['type'] == 'trunk':
proposed['voiceVlan'] = original['voiceVlan'] # API shouldn't include voice VLAN on a trunk port proposed['voiceVlan'] = original['voiceVlan'] # API shouldn't include voice VLAN on a trunk port
if meraki.is_update_required(original, proposed, optional_ignore=('number')): if meraki.is_update_required(original, proposed, optional_ignore=['number']):
path = meraki.construct_path('update', custom={'serial': meraki.params['serial'], path = meraki.construct_path('update', custom={'serial': meraki.params['serial'],
'number': meraki.params['number'], 'number': meraki.params['number'],
}) })

View file

@ -14,7 +14,7 @@
auth_key: '{{ auth_key }}' auth_key: '{{ auth_key }}'
host: marrrraki.com host: marrrraki.com
state: query state: query
org_name: DevTestOrg org_name: '{{test_org_name}}'
output_level: debug output_level: debug
delegate_to: localhost delegate_to: localhost
register: invalid_domain register: invalid_domain

View file

@ -1,5 +1,6 @@
--- ---
- block: - block:
# This is commented out because a device cannot be unclaimed via API
# - name: Claim a device into an organization # - name: Claim a device into an organization
# meraki_device: # meraki_device:
# auth_key: '{{auth_key}}' # auth_key: '{{auth_key}}'