From bb50fc3889ba2bd95c22e9bb65a51c22aab2ff20 Mon Sep 17 00:00:00 2001 From: RobW3LGA <42117788+RobW3LGA@users.noreply.github.com> Date: Wed, 22 May 2019 17:46:43 -0500 Subject: [PATCH] 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 --- lib/ansible/module_utils/network/aci/aci.py | 186 +++++++++++++++++++- 1 file changed, 185 insertions(+), 1 deletion(-) diff --git a/lib/ansible/module_utils/network/aci/aci.py b/lib/ansible/module_utils/network/aci/aci.py index 81cbdd68bbc..9c1adf5f87c 100644 --- a/lib/ansible/module_utils/network/aci/aci.py +++ b/lib/ansible/module_utils/network/aci/aci.py @@ -10,6 +10,7 @@ # Copyright: (c) 2017, Dag Wieers # 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): """