summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/aci.py
blob: b07f1b9b91e4b2b840052c36c15591ae07296a9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
# -*- coding: utf-8 -*-

# This code is part of Ansible, but is an independent component

# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.

# Copyright 2017 Dag Wieers <dag@wieers.com>
# Copyright 2017 Swetha Chunduri (@schunduri)
# All rights reserved.

# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright notice,
#      this list of conditions and the following disclaimer in the documentation
#      and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import json

from ansible.module_utils.urls import fetch_url
from ansible.module_utils._text import to_bytes

# Optional, only used for XML payload
try:
    import lxml.etree
    HAS_LXML_ETREE = True
except ImportError:
    HAS_LXML_ETREE = False

# Optional, only used for XML payload
try:
    from xmljson import cobra
    HAS_XMLJSON_COBRA = True
except ImportError:
    HAS_XMLJSON_COBRA = False


aci_argument_spec = dict(
    hostname=dict(type='str', required=True, aliases=['host']),
    username=dict(type='str', default='admin', aliases=['user']),
    password=dict(type='str', required=True, no_log=True),
    protocol=dict(type='str', removed_in_version='2.6'),  # Deprecated in v2.6
    timeout=dict(type='int', default=30),
    use_proxy=dict(type='bool', default=True),
    use_ssl=dict(type='bool', default=True),
    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'),
    export_policy=dict(aci_class='configExportP', mo='fabric/configexp-', 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'),
    import_policy=dict(aci_class='configImportP', mo='fabric/configimp-', key='name'),
    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'),
    snapshot=dict(aci_class='configSnapshot', mo='snapshot-', key='name'),
    snapshot_container=dict(aci_class='configSnapshotCont', mo='backupst/snapshots-', 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 '''
    result['error_code'] = 0
    result['error_text'] = 'Success'
    # Handle possible APIC error information
    if result['totalCount'] != '0':
        try:
            result['error_code'] = result['imdata'][0]['error']['attributes']['code']
            result['error_text'] = result['imdata'][0]['error']['attributes']['text']
        except (KeyError, IndexError):
            pass


def aci_response_json(result, rawoutput):
    ''' Handle APIC JSON response output '''
    try:
        result.update(json.loads(rawoutput))
    except Exception as e:
        # Expose RAW output for troubleshooting
        result.update(raw=rawoutput, error_code=-1, error_text="Unable to parse output as JSON, see 'raw' output. %s" % e)
        return

    # Handle possible APIC error information
    aci_response_error(result)


def aci_response_xml(result, rawoutput):
    ''' Handle APIC XML response output '''

    # NOTE: The XML-to-JSON conversion is using the "Cobra" convention
    try:
        xml = lxml.etree.fromstring(to_bytes(rawoutput))
        xmldata = cobra.data(xml)
    except Exception as e:
        # Expose RAW output for troubleshooting
        result.update(raw=rawoutput, error_code=-1, error_text="Unable to parse output as XML, see 'raw' output. %s" % e)
        return

    # Reformat as ACI does for JSON API output
    try:
        result.update(imdata=xmldata['imdata']['children'])
    except KeyError:
        result['imdata'] = dict()
    result['totalCount'] = xmldata['imdata']['attributes']['totalCount']

    # Handle possible APIC error information
    aci_response_error(result)


class ACIModule(object):

    def __init__(self, module):
        self.module = module
        self.params = module.params
        self.result = dict(changed=False)
        self.headers = None

        self.login()

    def define_protocol(self):
        ''' Set protocol based on use_ssl parameter '''

        # Set protocol for further use
        if self.params['protocol'] in ('http', 'https'):
            self.module.deprecate("Parameter 'protocol' is deprecated, please use 'use_ssl' instead.", '2.6')
        elif self.params['protocol'] is None:
            self.params['protocol'] = 'https' if self.params.get('use_ssl', True) else 'http'
        else:
            self.module.fail_json(msg="Parameter 'protocol' needs to be one of ( http, https )")

    def define_method(self):
        ''' Set method based on state parameter '''

        # Handle deprecated method/action parameter
        if self.params['method']:
            # Deprecate only if state was a valid option (not for aci_rest)
            if 'state' in self.module.argument_spec:
                self.module.deprecate("Parameter 'method' or 'action' is deprecated, please use 'state' instead", '2.6')
            method_map = dict(delete='absent', get='query', post='present')
            self.params['state'] = method_map[self.params['method']]
        else:
            state_map = dict(absent='delete', present='post', query='get')
            self.params['method'] = state_map[self.params['state']]

    def login(self):
        ''' Log in to APIC '''

        # Ensure protocol is set (only do this once)
        self.define_protocol()

        # Perform login request
        url = '%(protocol)s://%(hostname)s/api/aaaLogin.json' % self.params
        payload = {'aaaUser': {'attributes': {'name': self.params['username'], 'pwd': self.params['password']}}}
        resp, auth = fetch_url(self.module, url,
                               data=json.dumps(payload),
                               method='POST',
                               timeout=self.params['timeout'],
                               use_proxy=self.params['use_proxy'])

        # Handle APIC response
        if auth['status'] != 200:
            self.result['response'] = auth['msg']
            self.result['status'] = auth['status']
            try:
                # APIC error
                aci_response_json(self.result, auth['body'])
                self.module.fail_json(msg='Authentication failed: %(error_code)s %(error_text)s' % self.result, **self.result)
            except KeyError:
                # Connection error
                self.module.fail_json(msg='Authentication failed for %(url)s. %(msg)s' % auth)

        # Retain cookie for later use
        self.headers = dict(Cookie=resp.headers['Set-Cookie'])

    def request(self, path, payload=None):
        ''' Perform a REST request '''

        # Ensure method is set (only do this once)
        self.define_method()

        # Perform request
        self.result['url'] = '%(protocol)s://%(hostname)s/' % self.params + path.lstrip('/')
        resp, info = fetch_url(self.module, self.result['url'],
                               data=payload,
                               headers=self.headers,
                               method=self.params['method'].upper(),
                               timeout=self.params['timeout'],
                               use_proxy=self.params['use_proxy'])

        self.result['response'] = info['msg']
        self.result['status'] = info['status']

        # Handle APIC response
        if info['status'] != 200:
            try:
                # APIC error
                aci_response_json(self.result, info['body'])
                self.module.fail_json(msg='Request failed: %(error_code)s %(error_text)s' % self.result, **self.result)
            except KeyError:
                # Connection error
                self.module.fail_json(msg='Request failed for %(url)s. %(msg)s' % info)

        aci_response_json(self.result, resp.read())

    def query(self, path):
        ''' Perform a query with no payload '''
        url = '%(protocol)s://%(hostname)s/' % self.params + path.lstrip('/')
        resp, query = fetch_url(self.module, url,
                                data=None,
                                headers=self.headers,
                                method='GET',
                                timeout=self.params['timeout'],
                                use_proxy=self.params['use_proxy'])

        # Handle APIC response
        if query['status'] != 200:
            self.result['response'] = query['msg']
            self.result['status'] = query['status']
            try:
                # APIC error
                aci_response_json(self.result, query['body'])
                self.module.fail_json(msg='Query failed: %(error_code)s %(error_text)s' % self.result, **self.result)
            except KeyError:
                # Connection error
                self.module.fail_json(msg='Query failed for %(url)s. %(msg)s' % query)

        query = json.loads(resp.read())

        return json.dumps(query['imdata'], sort_keys=True, indent=2) + '\n'

    def request_diff(self, path, payload=None):
        ''' Perform a request, including a proper diff output '''
        self.result['diff'] = dict()
        self.result['diff']['before'] = self.query(path)
        self.request(path, payload=payload)
        # TODO: Check if we can use the request output for the 'after' diff
        self.result['diff']['after'] = self.query(path)

        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
        the object exists, and if check_mode is False. A successful change will mark the module as changed.
        """
        self.result['proposed'] = {}

        if not self.result['existing']:
            return

        elif not self.module.check_mode:
            resp, info = fetch_url(self.module, self.result['url'],
                                   headers=self.headers,
                                   method='DELETE',
                                   timeout=self.params['timeout'],
                                   use_proxy=self.params['use_proxy'])

            self.result['response'] = info['msg']
            self.result['status'] = info['status']
            self.result['method'] = 'DELETE'

            # Handle APIC response
            if info['status'] == 200:
                self.result['changed'] = True
                aci_response_json(self.result, resp.read())
            else:
                try:
                    # APIC error
                    aci_response_json(self.result, info['body'])
                    self.module.fail_json(msg='Request failed: %(error_code)s %(error_text)s' % self.result, **self.result)
                except KeyError:
                    # Connection error
                    self.module.fail_json(msg='Request failed for %(url)s. %(msg)s' % info)
        else:
            self.result['changed'] = True
            self.result['method'] = 'DELETE'

    def get_diff(self, aci_class):
        """
        This method is used to get the difference between the proposed and existing configurations. Each module
        should call the get_existing method before this method, and add the proposed config to the module results
        using the module's config parameters. The new config will added to the self.result dictionary.

        :param aci_class: Type str.
                          This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
        """
        proposed_config = self.result['proposed'][aci_class]['attributes']
        if self.result['existing']:
            existing_config = self.result['existing'][0][aci_class]['attributes']
            config = {}

            # values are strings, so any diff between proposed and existing can be a straight replace
            for key, value in proposed_config.items():
                existing_field = existing_config.get(key)
                if value != existing_field:
                    config[key] = value

            # add name back to config only if the configs do not match
            if config:
                # TODO: If URLs are built with the object's name, then we should be able to leave off adding the name back
                # config["name"] = proposed_config["name"]
                config = {aci_class: {'attributes': config}}

            # check for updates to child configs and update new config dictionary
            children = self.get_diff_children(aci_class)
            if children and config:
                config[aci_class].update({'children': children})
            elif children:
                config = {aci_class: {'attributes': {}, 'children': children}}

        else:
            config = self.result['proposed']

        self.result['config'] = config

    @staticmethod
    def get_diff_child(child_class, proposed_child, existing_child):
        """
        This method is used to get the difference between a proposed and existing child configs. The get_nested_config()
        method should be used to return the proposed and existing config portions of child.

        :param child_class: Type str.
                            The root class (dict key) for the child dictionary.
        :param proposed_child: Type dict.
                               The config portion of the proposed child dictionary.
        :param existing_child: Type dict.
                               The config portion of the existing child dictionary.
        :return: The child config with only values that are updated. If the proposed dictionary has no updates to make
                 to what exists on the APIC, then None is returned.
        """
        update_config = {child_class: {'attributes': {}}}
        for key, value in proposed_child.items():
            if value != existing_child[key]:
                update_config[child_class]['attributes'][key] = value

        if not update_config[child_class]['attributes']:
            return None

        return update_config

    def get_diff_children(self, aci_class):
        """
        This method is used to retrieve the updated child configs by comparing the proposed children configs
        agains the objects existing children configs.

        :param aci_class: Type str.
                          This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
        :return: The list of updated child config dictionaries. None is returned if there are no changes to the child
                 configurations.
        """
        proposed_children = self.result['proposed'][aci_class].get('children')
        if proposed_children:
            child_updates = []
            existing_children = self.result['existing'][0][aci_class].get('children', [])

            # Loop through proposed child configs and compare against existing child configuration
            for child in proposed_children:
                child_class, proposed_child, existing_child = self.get_nested_config(child, existing_children)

                if existing_child is None:
                    child_update = child
                else:
                    child_update = self.get_diff_child(child_class, proposed_child, existing_child)

                # Update list of updated child configs only if the child config is different than what exists
                if child_update:
                    child_updates.append(child_update)
        else:
            return None

        return child_updates

    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.
        """
        uri = self.result['url'] + self.result['filter_string']

        resp, info = fetch_url(self.module, uri,
                               headers=self.headers,
                               method='GET',
                               timeout=self.params['timeout'],
                               use_proxy=self.params['use_proxy'])
        self.result['response'] = info['msg']
        self.result['status'] = info['status']
        self.result['method'] = 'GET'

        # Handle APIC response
        if info['status'] == 200:
            self.result['existing'] = json.loads(resp.read())['imdata']
        else:
            try:
                # APIC error
                aci_response_json(self.result, info['body'])
                self.module.fail_json(msg='Request failed: %(error_code)s %(error_text)s' % self.result, **self.result)
            except KeyError:
                # Connection error
                self.module.fail_json(msg='Request failed for %(url)s. %(msg)s' % info)

    @staticmethod
    def get_nested_config(proposed_child, existing_children):
        """
        This method is used for stiping off the outer layers of the child dictionaries so only the configuration
        key, value pairs are returned.

        :param proposed_child: Type dict.
                               The dictionary that represents the child config.
        :param existing_children: Type list.
                                  The list of existing child config dictionaries.
        :return: The child's class as str (root config dict key), the child's proposed config dict, and the child's
                 existing configuration dict.
        """
        for key in proposed_child.keys():
            child_class = key
            proposed_config = proposed_child[key]['attributes']
            existing_config = None

            # get existing dictionary from the list of existing to use for comparison
            for child in existing_children:
                if child.get(child_class):
                    existing_config = child[key]['attributes']
                    break

        return child_class, proposed_config, existing_config

    def payload(self, aci_class, class_config, child_configs=None):
        """
        This method is used to dynamically build the proposed configuration dictionary from the config related parameters
        passed into the module. All values that were not passed values from the playbook task will be removed so as to not
        inadvertently change configurations.

        :param aci_class: Type str
                          This is the root dictionary key for the MO's configuration body, or the ACI class of the MO.
        :param class_config: Type dict
                             This is the configuration of the MO using the dictionary keys expected by the API
        :param child_configs: Type list
                              This is a list of child dictionaries associated with the MOs config. The list should only
                              include child objects that are used to associate two MOs together. Children that represent
                              MOs should have their own module.
        """
        proposed = dict((k, str(v)) for k, v in class_config.items() if v is not None)
        self.result['proposed'] = {aci_class: {'attributes': proposed}}

        # add child objects to proposed
        if child_configs:
            children = []
            for child in child_configs:
                has_value = False
                for root_key in child.keys():
                    for final_keys, values in child[root_key]['attributes'].items():
                        if values is None:
                            child[root_key]['attributes'].pop(final_keys)
                        else:
                            child[root_key]['attributes'][final_keys] = str(values)
                            has_value = True
                if has_value:
                    children.append(child)

            if children:
                self.result['proposed'][aci_class].update(dict(children=children))

    def post_config(self):
        """
        This method is used to handle the logic when the modules state is equal to present. The method only pushes a change if
        the object has differences than what exists on the APIC, and if check_mode is False. A successful change will mark the
        module as changed.
        """
        if not self.result['config']:
            return
        elif not self.module.check_mode:
            resp, info = fetch_url(self.module, self.result['url'],
                                   data=json.dumps(self.result['config']),
                                   headers=self.headers,
                                   method='POST',
                                   timeout=self.params['timeout'],
                                   use_proxy=self.params['use_proxy'])

            self.result['response'] = info['msg']
            self.result['status'] = info['status']
            self.result['method'] = 'POST'

            # Handle APIC response
            if info['status'] == 200:
                self.result['changed'] = True
                aci_response_json(self.result, resp.read())
            else:
                try:
                    # APIC error
                    aci_response_json(self.result, info['body'])
                    self.module.fail_json(msg='Request failed: %(error_code)s %(error_text)s' % self.result, **self.result)
                except KeyError:
                    # Connection error
                    self.module.fail_json(msg='Request failed for %(url)s. %(msg)s' % info)
        else:
            self.result['changed'] = True
            self.result['method'] = 'POST'