From ddd3e6fb91ac98727c9f5ee83cb8cd472f830431 Mon Sep 17 00:00:00 2001 From: Jacob McGill Date: Thu, 24 Aug 2017 20:37:34 -0400 Subject: [PATCH] ACI: Update ACIModule to dynamically build URLs (#28618) --- lib/ansible/module_utils/aci.py | 266 +++++++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 5 deletions(-) diff --git a/lib/ansible/module_utils/aci.py b/lib/ansible/module_utils/aci.py index a42356b5b22..267b5342c77 100644 --- a/lib/ansible/module_utils/aci.py +++ b/lib/ansible/module_utils/aci.py @@ -61,6 +61,39 @@ aci_argument_spec = dict( validate_certs=dict(type='bool', default=True), ) +URL_MAPPING = dict( + action_rule=dict(aci_class='rtctrlAttrP', mo='attr-', key='name'), + aep=dict(aci_class='infraAttEntityP', mo='infra/attentp-', key='name'), + ap=dict(aci_class='fvAp', mo='ap-', key='name'), + bd=dict(aci_class='fvBD', mo='BD-', key='name'), + bd_l3out=dict(aci_class='fvRsBDToOut', mo='rsBDToOut-', key='tnL3extOutName'), + contract=dict(aci_class='vzBrCP', mo='brc-', key='name'), + entry=dict(aci_class='vzEntry', mo='e-', key='name'), + epg=dict(aci_class='fvAEPg', mo='epg-', key='name'), + epg_consumer=dict(aci_class='fvRsCons', mo='rscons-', key='tnVzBrCPName'), + epg_domain=dict(aci_class='fvRsDomAtt', mo='rsdomAtt-', key='tDn'), + epg_provider=dict(aci_class='fvRsProv', mo='rsprov-', key='tnVzBrCPName'), + epr_policy=dict(aci_class='fvEpRetPol', mo='epRPol-', key='name'), + fc_policy=dict(aci_class='fcIfPol', mo='infra/fcIfPol-', key='name'), + filter=dict(aci_class='vzFilter', mo='flt-', key='name'), + gateway_addr=dict(aci_class='fvSubnet', mo='subnet-', key='ip'), + l2_policy=dict(aci_class='l2IfPol', mo='infra/l2IfP-', key='name'), + lldp_policy=dict(aci_class='lldpIfPol', mo='infra/lldpIfP-', key='name'), + mcp=dict(aci_class='mcpIfPol', mo='infra/mcpIfP-', key='name'), + monitoring_policy=dict(aci_class='monEPGPol', mo='monepg-', key='name'), + port_channel=dict(aci_class='lacpLagPol', mo='infra/lacplagp-', key='name'), + port_security=dict(aci_class='l2PortSecurityPol', mo='infra/portsecurityP-', key='name'), + rtp=dict(aci_class='l3extRouteTagPol', mo='rttag-', key='name'), + subject=dict(aci_class='vzSubj', mo='subj-', key='name'), + subject_filter=dict(aci_class='vzRsSubjFiltAtt', mo='rssubjFiltAtt-', key='tnVzFilterName'), + taboo_contract=dict(aci_class='vzTaboo', mo='taboo-', key='name'), + tenant=dict(aci_class='fvTenant', mo='tn-', key='name'), + tenant_span_dst_grp=dict(aci_class='spanDestGrp', mo='destgrp-', key='name'), + tenant_span_src_grp=dict(aci_class='spanSrcGrp', mo='srcgrp-', key='name'), + tenant_span_src_grp_dst_grp=dict(aci_class='spanSpanLbl', mo='spanlbl-', key='name'), + vrf=dict(aci_class='fvCtx', mo='ctx-', key='name'), +) + def aci_response_error(result): ''' Set error information when found ''' @@ -243,6 +276,231 @@ class ACIModule(object): if self.result['diff']['before'] != self.result['diff']['after']: self.result['changed'] = True + 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. + + :param root_class: Type str. + The top-level class naming parameter per the modules (EX: tenant). + :param sublass_1: Type str. + The second-level class naming parameter per the modules (EX: bd). + :param sublass_2: Type str. + The third-level class naming parameter per the modules (EX: gateway). + :param sublass_3: Type str. + The fourth-level class naming parameter per the modules. + :param child_classes: Type tuple. + The list of child classes that the module supports along with the object. + :return: The path and filter_string needed to build the full URL. + """ + if child_classes is None: + child_includes = '' + else: + child_includes = ','.join(child_classes) + child_includes = '&rsp-subtree=full&rsp-subtree-class=' + child_includes + + if subclass_3 is not None: + path, filter_string = self._construct_url_4(root_class, subclass_1, subclass_2, subclass_3, child_includes) + elif subclass_2 is not None: + path, filter_string = self._construct_url_3(root_class, subclass_1, subclass_2, child_includes) + elif subclass_1 is not None: + path, filter_string = self._construct_url_2(root_class, subclass_1, child_includes) + else: + path, filter_string = self._construct_url_1(root_class, child_includes) + + self.result['url'] = '{}://{}/{}'.format(self.module.params['protocol'], self.module.params['hostname'], path) + self.result['filter_string'] = filter_string + + def _construct_url_1(self, obj_class, child_includes): + """ + This method is used by get_url when the object is the top-level class. + """ + obj = self.module.params.get(obj_class) + obj_dict = URL_MAPPING[obj_class] + obj_class = obj_dict['aci_class'] + obj_mo = obj_dict['mo'] + + # State is present or absent + if self.module.params['state'] != 'query': + path = 'api/mo/uni/{}[{}].json'.format(obj_mo, obj) + filter_string = '?rsp-prop-include=config-only' + child_includes + # Query for all objects of the module's class + elif obj is None: + path = 'api/class/{}.json'.format(obj_class) + filter_string = '' + # Query for a specific object in the module's class + else: + path = 'api/mo/uni/{}[{}].json'.format(obj_mo, obj) + filter_string = '' + + # Append child_includes to filter_string if filter string is empty + if child_includes is not None and filter_string == '': + filter_string = child_includes.replace('&', '?', 1) + + return path, filter_string + + def _construct_url_2(self, parent_class, obj_class, child_includes): + """ + This method is used by get_url when the object is the second-level class. + """ + parent = self.module.params.get(parent_class) + parent_dict = URL_MAPPING[parent_class] + parent_class = parent_dict['aci_class'] + parent_mo = parent_dict['mo'] + obj = self.module.params.get(obj_class) + obj_dict = URL_MAPPING[obj_class] + obj_class = obj_dict['aci_class'] + obj_mo = obj_dict['mo'] + obj_key = obj_dict['key'] + + if not child_includes: + self_child_includes = '?rsp-subtree=full&rsp-subtree-class=' + obj_class + else: + self_child_includes = child_includes.replace('&', '?', 1) + ',' + obj_class + + # State is present or absent + if self.module.params['state'] != 'query': + path = 'api/mo/uni/{}[{}]/{}[{}].json'.format(parent_mo, parent, obj_mo, obj) + filter_string = '?rsp-prop-include=config-only' + child_includes + # Query for all objects of the module's class + elif obj is None and parent is None: + path = 'api/class/{}.json'.format(obj_class) + filter_string = '' + # Queries when parent object is provided + elif parent is not None: + # Query for specific object in the module's class + if obj is not None: + path = 'api/mo/uni/{}[{}]/{}[{}].json'.format(parent_mo, parent, obj_mo, obj) + filter_string = '' + # Query for all object's of the module's class that belong to a specific parent object + else: + path = 'api/mo/uni/{}[{}].json'.format(parent_mo, parent) + filter_string = self_child_includes + # Query for all objects of the module's class that match the provided ID value + else: + path = 'api/class/{}.json'.format(obj_class) + filter_string = '?query-target-filter=eq({}.{}, \"{}\")'.format(obj_class, obj_key, obj) + child_includes + + # Append child_includes to filter_string if filter string is empty + if child_includes is not None and filter_string == '': + filter_string = child_includes.replace('&', '?', 1) + + return path, filter_string + + def _construct_url_3(self, root_class, parent_class, obj_class, child_includes): + """ + This method is used by get_url when the object is the third-level class. + """ + root = self.module.params.get(root_class) + root_dict = URL_MAPPING[root_class] + root_class = root_dict['aci_class'] + root_mo = root_dict['mo'] + parent = self.module.params.get(parent_class) + parent_dict = URL_MAPPING[parent_class] + parent_class = parent_dict['aci_class'] + parent_mo = parent_dict['mo'] + parent_key = parent_dict['key'] + obj = self.module.params.get(obj_class) + obj_dict = URL_MAPPING[obj_class] + obj_class = obj_dict['aci_class'] + obj_mo = obj_dict['mo'] + obj_key = obj_dict['key'] + + if not child_includes: + self_child_includes = '&rsp-subtree=full&rsp-subtree-class=' + obj_class + else: + self_child_includes = '{},{}'.format(child_includes, obj_class) + + if not child_includes: + parent_self_child_includes = '&rsp-subtree=full&rsp-subtree-class={},{}'.format(parent_class, obj_class) + else: + parent_self_child_includes = '{},{},{}'.format(child_includes, parent_class, obj_class) + + # State is ablsent or present + if self.module.params['state'] != 'query': + path = 'api/mo/uni/{}[{}]/{}[{}]/{}[{}].json'.format(root_mo, root, parent_mo, parent, obj_mo, obj) + filter_string = '?rsp-prop-include=config-only' + child_includes + # Query for all objects of the module's class + elif obj is None and parent is None and root is None: + path = 'api/class/{}.json'.format(obj_class) + filter_string = '' + # Queries when root object is provided + elif root is not None: + # Queries when parent object is provided + if parent is not None: + # Query for a specific object of the module's class + if obj is not None: + path = 'api/mo/uni/{}[{}]/{}[{}]/{}[{}].json'.format(root_mo, root, parent_mo, parent, obj_mo, obj) + filter_string = '' + # Query for all objects of the module's class that belong to a specific parent object + else: + path = 'api/mo/uni/{}[{}]/{}[{}].json'.format(root_mo, root, parent_mo, parent) + filter_string = self_child_includes.replace('&', '?', 1) + # Query for all objects of the module's class that match the provided ID value and belong to a specefic root object + elif obj is not None: + path = 'api/mo/uni/{}[{}].json'.format(root_mo, root) + filter_string = '?rsp-subtree-filter=eq({}.{}, \"{}\"){}'.format(obj_class, obj_key, obj, self_child_includes) + # Query for all objects of the module's class that belong to a specific root object + else: + path = 'api/mo/uni/{}[{}].json'.format(root_mo, root) + filter_string = '?' + parent_self_child_includes + # Queries when parent object is provided but root object is not provided + elif parent is not None: + # Query for all objects of the module's class that belong to any parent class + # matching the provided ID values for both object and parent object + if obj is not None: + path = 'api/class/{}.json'.format(parent_class) + filter_string = '?query-target-filter=eq({}.{}, \"{}\"){}&rsp-subtree-filter=eq({}.{}, \"{}\")'.format( + parent_class, parent_key, parent, self_child_includes, obj_class, obj_key, obj) + # Query for all objects of the module's class that belong to any parent class + # matching the provided ID value for the parent object + else: + path = 'api/class/{}.json'.format(parent_class) + filter_string = '?query-target-filter=eq({}.{}, \"{}\"){}'.format(parent_class, parent_key, parent, self_child_includes) + # Query for all objects of the module's class matching the provided ID value of the object + else: + path = 'api/class/{}.json'.format(obj_class) + filter_string = '?query-target-filter=eq({}.{}, \"{}\")'.format(obj_class, obj_key, obj) + child_includes + + # append child_includes to filter_string if filter string is empty + if child_includes is not None and filter_string == '': + filter_string = child_includes.replace('&', '?', 1) + + return path, filter_string + + def _construct_url_4(self, root_class, sec_class, parent_class, obj_class, child_includes): + """ + This method is used by get_url when the object is the third-level class. + """ + root = self.module.params.get(root_class) + root_dict = URL_MAPPING[root_class] + root_class = root_dict['aci_class'] + root_mo = root_dict['mo'] + sec = self.module.params.get(sec_class) + sec_dict = URL_MAPPING[sec_class] + sec_class = sec_dict['aci_class'] + sec_mo = sec_dict['mo'] + # sec_key = sec_dict['key'] + parent = self.module.params.get(parent_class) + parent_dict = URL_MAPPING[parent_class] + parent_class = parent_dict['aci_class'] + parent_mo = parent_dict['mo'] + # parent_key = parent_dict['key'] + obj = self.module.params.get(obj_class) + obj_dict = URL_MAPPING[obj_class] + obj_class = obj_dict['aci_class'] + obj_mo = obj_dict['mo'] + # obj_key = obj_dict['key'] + + # State is ablsent or present + if self.module.params['state'] != 'query': + path = 'api/mo/uni/{}[{}]/{}[{}]/{}[{}]/{}[{}].json'.format(root_mo, root, sec_mo, sec, parent_mo, parent, obj_mo, obj) + filter_string = '?rsp-prop-include=config-only' + child_includes + else: + path = 'api/class/{}.json'.format(obj_class) + filter_string = child_includes + + return path, filter_string + def delete_config(self): """ This method is used to handle the logic when the modules state is equal to absent. The method only pushes a change if @@ -375,18 +633,16 @@ class ACIModule(object): return child_updates - def get_existing(self, filter_string=""): + def get_existing(self): """ This method is used to get the existing object(s) based on the path specified in the module. Each module should build the URL so that if the object's name is supplied, then it will retrieve the configuration for that particular object, but if no name is supplied, then it will retrieve all MOs for the class. Following this method will ensure that this method can be used to supply the existing configuration when using the get_diff method. The response, status, and existing configuration will be added to the self.result dictionary. - - :param filter_string: Type str. - The filter to use in order to retrieve the filtered configuration. """ - uri = self.result['url'] + filter_string + uri = self.result['url'] + self.result['filter_string'] + resp, info = fetch_url(self.module, uri, headers=self.headers, method='GET',