construct_deep_url() (#53475)

* Updates aci.py with the ability to add ACI objects to any depth

Changes start at line 411 (construct_deep_url() and supporting functions). One minor change to line 633 (the original construct_url()) to provide for testability:  ...join(sorted(self.child_classes)) vs ...join(self.child_classes)

I am also attaching two test files. One characterizing the existing construct_url() and the matching test set for construct_deep_url() to support my efforts and proof of parity

* Two PyTest files to support construct_deep_url

These two files provide testing parity, one characterizing the original construct_url() function and the other proofing construct_deep_url(). The ...deep_url.py test file goes five layers deep to provide better validation for the function

* Correcting previous upload to incorrect folder

These two files provide testing parity, one characterizing the original construct_url() function and the other proofing construct_deep_url(). The ...deep_url.py test file goes five layers deep to provide better validation for the function

* Deleting for file name change per Matt Clay

* Deleting for file name change per Matt Clay

* Correcting file names per Matt Clay

@mattclay Thanks again for your continued guidance and patience. Please cancel the previous (incorrect) request

* Wrong location for test file

* Wrong location for test file

* First attempt to comply with suggestions

lib/ansible/module_utils/network/aci/aci.py:517:0: SyntaxWarning: "is not" with a literal. Did you mean "!="?
lib/ansible/module_utils/network/aci/aci.py:534:0: SyntaxWarning: "is not" with a literal. Did you mean "!="?
lib/ansible/module_utils/network/aci/aci.py:558:161: E501 line too long (210 > 160 characters)

* First attempt to comply with suggestions

test/units/module_utils/network/aci/test_aci_construct_url.py:1:14: SyntaxError: import pytest
test/units/module_utils/network/aci/test_aci_deep_url.py:1:14: SyntaxError: import pytest
test/units/module_utils/network/aci/test_aci_construct_url.py:0:0: use "\n" for line endings instead of "\r\n"
test/units/module_utils/network/aci/test_aci_deep_url.py:0:0: use "\n" for line endings instead of "\r\n"
Shortened test function names (less descriptive)

* Second attempt to comply with suggestions

* Second attempt to comply with suggestions

* Third attempt to comply with suggestions

* Third attempt to comply with suggestions

* Pro Tip: Convert from 'CRLF' to 'LF' in VSCode

It's on the status bar to the right

* Added setup() support for tests

* Continued corrections to support testing

* Added two mocks to support testing

I could not find where to place fakes/mocks, so please let me know if the current location is incorrect

* Adding tmpdir property to mock_basic.py

* Added last blank line to mock_basic.py

To pass sanity test

* Attempt to correct setup() issues

* Attempt to correct setup() issues

* Attempt to correct setup() issues

* Attempt to correct setup() issues

* Withdrawing pending injectability tweak to aci.py

* Withdrawing pending injectability tweak to aci.py

* Withdrawing pending injectability tweak to aci.py

* Withdrawing pending injectability tweak to aci.py
This commit is contained in:
RobW3LGA 2019-05-22 17:46:43 -05:00 committed by Dag Wieers
parent 391a1042c2
commit bb50fc3889

View file

@ -10,6 +10,7 @@
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
# Copyright: (c) 2017, Jacob McGill (@jmcgill298)
# Copyright: (c) 2017, Swetha Chunduri (@schunduri)
# Copyright: (c) 2019, Rob Huelga (@RobW3LGA)
# All rights reserved.
# Redistribution and use in source and binary forms, with or without modification,
@ -412,6 +413,189 @@ class ACIModule(object):
elif len(accepted_params) > 1:
return 'and(' + ','.join(['eq({0}.{1}, "{2}")'.format(obj_class, k, v) for (k, v) in accepted_params.items()]) + ')'
def _deep_url_path_builder(self, obj):
target_class = obj['target_class']
target_filter = obj['target_filter']
subtree_class = obj['subtree_class']
subtree_filter = obj['subtree_filter']
object_rn = obj['object_rn']
mo = obj['module_object']
add_subtree_filter = obj['add_subtree_filter']
add_target_filter = obj['add_target_filter']
if self.module.params['state'] in ('absent', 'present') and mo is not None:
self.path = 'api/mo/uni/{0}.json'.format(object_rn)
self.update_qs({'rsp-prop-include': 'config-only'})
else:
# State is 'query'
if object_rn is not None:
# Query for a specific object in the module's class
self.path = 'api/mo/uni/{0}.json'.format(object_rn)
else:
self.path = 'api/class/{0}.json'.format(target_class)
if add_target_filter:
self.update_qs(
{'query-target-filter': self.build_filter(target_class, target_filter)})
if add_subtree_filter:
self.update_qs(
{'rsp-subtree-filter': self.build_filter(subtree_class, subtree_filter)})
if 'port' in self.params and self.params['port'] is not None:
self.url = '{protocol}://{host}:{port}/{path}'.format(
path=self.path, **self.module.params)
else:
self.url = '{protocol}://{host}/{path}'.format(
path=self.path, **self.module.params)
if self.child_classes:
self.update_qs(
{'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(sorted(self.child_classes))})
def _deep_url_parent_object(self, parent_objects, parent_class):
for parent_object in parent_objects:
if parent_object['aci_class'] is parent_class:
return parent_object
return None
def construct_deep_url(self, target_object, parent_objects=None, child_classes=None):
"""
This method is used to retrieve the appropriate URL path and filter_string to make the request to the APIC.
:param target_object: The target class dictionary containing parent_class, aci_class, aci_rn, target_filter, and module_object keys.
:param parent_objects: The parent class list of dictionaries containing parent_class, aci_class, aci_rn, target_filter, and module_object keys.
:param child_classes: The list of child classes that the module supports along with the object.
:type target_object: dict
:type parent_objects: list[dict]
:type child_classes: list[string]
:return: The path and filter_string needed to build the full URL.
"""
self.filter_string = ''
rn_builder = None
subtree_classes = None
add_subtree_filter = False
add_target_filter = False
has_target_query = False
has_target_query_compare = False
has_target_query_difference = False
has_target_query_called = False
if child_classes is None:
self.child_classes = set()
else:
self.child_classes = set(child_classes)
target_parent_class = target_object['parent_class']
target_class = target_object['aci_class']
target_rn = target_object['aci_rn']
target_filter = target_object['target_filter']
target_module_object = target_object['module_object']
url_path_object = dict(
target_class=target_class,
target_filter=target_filter,
subtree_class=target_class,
subtree_filter=target_filter,
module_object=target_module_object
)
if target_module_object is not None:
rn_builder = target_rn
else:
has_target_query = True
has_target_query_compare = True
if parent_objects is not None:
current_parent_class = target_parent_class
has_parent_query_compare = False
has_parent_query_difference = False
is_first_parent = True
is_single_parent = None
search_classes = set()
while current_parent_class != 'uni':
parent_object = self._deep_url_parent_object(
parent_objects=parent_objects, parent_class=current_parent_class)
if parent_object is not None:
parent_parent_class = parent_object['parent_class']
parent_class = parent_object['aci_class']
parent_rn = parent_object['aci_rn']
parent_filter = parent_object['target_filter']
parent_module_object = parent_object['module_object']
if is_first_parent:
is_single_parent = True
else:
is_single_parent = False
is_first_parent = False
if parent_parent_class != 'uni':
search_classes.add(parent_class)
if parent_module_object is not None:
if rn_builder is not None:
rn_builder = '{0}/{1}'.format(parent_rn,
rn_builder)
else:
rn_builder = parent_rn
url_path_object['target_class'] = parent_class
url_path_object['target_filter'] = parent_filter
has_target_query = False
else:
rn_builder = None
subtree_classes = search_classes
has_target_query = True
if is_single_parent:
has_parent_query_compare = True
current_parent_class = parent_parent_class
else:
raise ValueError("Reference error for parent_class '{0}'. Each parent_class must reference a valid object".format(current_parent_class))
if not has_target_query_difference and not has_target_query_called:
if has_target_query is not has_target_query_compare:
has_target_query_difference = True
else:
if not has_parent_query_difference and has_target_query is not has_parent_query_compare:
has_parent_query_difference = True
has_target_query_called = True
if not has_parent_query_difference and has_parent_query_compare and target_module_object is not None:
add_target_filter = True
elif has_parent_query_difference and target_module_object is not None:
add_subtree_filter = True
self.child_classes.add(target_class)
if has_target_query:
add_target_filter = True
elif has_parent_query_difference and not has_target_query and target_module_object is None:
self.child_classes.add(target_class)
self.child_classes.update(subtree_classes)
elif not has_parent_query_difference and not has_target_query and target_module_object is None:
self.child_classes.add(target_class)
elif not has_target_query and is_single_parent and target_module_object is None:
self.child_classes.add(target_class)
url_path_object['object_rn'] = rn_builder
url_path_object['add_subtree_filter'] = add_subtree_filter
url_path_object['add_target_filter'] = add_target_filter
self._deep_url_path_builder(url_path_object)
def construct_url(self, root_class, subclass_1=None, subclass_2=None, subclass_3=None, child_classes=None):
"""
This method is used to retrieve the appropriate URL path and filter_string to make the request to the APIC.
@ -451,7 +635,7 @@ class ACIModule(object):
if self.child_classes:
# Append child_classes to filter_string if filter string is empty
self.update_qs({'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(self.child_classes)})
self.update_qs({'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(sorted(self.child_classes))})
def _construct_url_1(self, obj):
"""