summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils
diff options
context:
space:
mode:
authorAnsible Core Team <info@ansible.com>2020-03-09 09:40:34 +0000
committerAnsible Core Team <info@ansible.com>2020-03-09 09:40:34 +0000
commitad5be6f8fd604ad53c784af148965b20459884af (patch)
treeba0ee59f1423a6c0e65ae99ccce5e47f2725b007 /lib/ansible/module_utils
parentaee8e4f5ff0cf2dcc25a2a72f446add6367bf539 (diff)
downloadansible-ad5be6f8fd604ad53c784af148965b20459884af.tar.gz
Migrated to cisco.aci
Diffstat (limited to 'lib/ansible/module_utils')
-rw-r--r--lib/ansible/module_utils/network/aci/aci.py1164
1 files changed, 0 insertions, 1164 deletions
diff --git a/lib/ansible/module_utils/network/aci/aci.py b/lib/ansible/module_utils/network/aci/aci.py
deleted file mode 100644
index 313d71de97..0000000000
--- a/lib/ansible/module_utils/network/aci/aci.py
+++ /dev/null
@@ -1,1164 +0,0 @@
-# -*- 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: (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,
-# 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.
-
-from __future__ import (absolute_import, division, print_function)
-__metaclass__ = type
-
-import base64
-import json
-import os
-from copy import deepcopy
-
-from ansible.module_utils.parsing.convert_bool import boolean
-from ansible.module_utils.urls import fetch_url
-from ansible.module_utils._text import to_bytes, to_native
-
-# Optional, only used for APIC signature-based authentication
-try:
- from OpenSSL.crypto import FILETYPE_PEM, load_privatekey, sign
- HAS_OPENSSL = True
-except ImportError:
- HAS_OPENSSL = False
-
-# 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
-
-
-def aci_argument_spec():
- return dict(
- host=dict(type='str', required=True, aliases=['hostname']),
- port=dict(type='int', required=False),
- username=dict(type='str', default='admin', aliases=['user']),
- password=dict(type='str', no_log=True),
- private_key=dict(type='str', aliases=['cert_key'], no_log=True), # Beware, this is not the same as client_key !
- certificate_name=dict(type='str', aliases=['cert_name']), # Beware, this is not the same as client_cert !
- output_level=dict(type='str', default='normal', choices=['debug', 'info', 'normal']),
- 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),
- )
-
-
-class ACIModule(object):
-
- def __init__(self, module):
- self.module = module
- self.params = module.params
- self.result = dict(changed=False)
- self.headers = dict()
- self.child_classes = set()
-
- # error output
- self.error = dict(code=None, text=None)
-
- # normal output
- self.existing = None
-
- # info output
- self.config = dict()
- self.original = None
- self.proposed = dict()
-
- # debug output
- self.filter_string = ''
- self.method = None
- self.path = None
- self.response = None
- self.status = None
- self.url = None
-
- # aci_rest output
- self.imdata = None
- self.totalCount = None
-
- # Ensure protocol is set
- self.define_protocol()
-
- if self.module._debug:
- self.module.warn('Enable debug output because ANSIBLE_DEBUG was set.')
- self.params['output_level'] = 'debug'
-
- if self.params.get('private_key'):
- # Perform signature-based authentication, no need to log on separately
- if not HAS_OPENSSL:
- self.module.fail_json(msg='Cannot use signature-based authentication because pyopenssl is not available')
- elif self.params.get('password') is not None:
- self.module.warn("When doing ACI signatured-based authentication, providing parameter 'password' is not required")
- elif self.params.get('password'):
- # Perform password-based authentication, log on using password
- self.login()
- else:
- self.module.fail_json(msg="Either parameter 'password' or 'private_key' is required for authentication")
-
- def boolean(self, value, true='yes', false='no'):
- ''' Return an acceptable value back '''
-
- # When we expect value is of type=bool
- if value is None:
- return None
- elif value is True:
- return true
- elif value is False:
- return false
-
- # If all else fails, escalate back to user
- self.module.fail_json(msg="Boolean value '%s' is an invalid ACI boolean value.")
-
- def iso8601_format(self, dt):
- ''' Return an ACI-compatible ISO8601 formatted time: 2123-12-12T00:00:00.000+00:00 '''
- try:
- return dt.isoformat(timespec='milliseconds')
- except Exception:
- tz = dt.strftime('%z')
- return '%s.%03d%s:%s' % (dt.strftime('%Y-%m-%dT%H:%M:%S'), dt.microsecond / 1000, tz[:3], tz[3:])
-
- def define_protocol(self):
- ''' Set protocol based on use_ssl parameter '''
-
- # Set protocol for further use
- self.params['protocol'] = 'https' if self.params.get('use_ssl', True) else 'http'
-
- def define_method(self):
- ''' Set method based on state parameter '''
-
- # Set method for further use
- state_map = dict(absent='delete', present='post', query='get')
- self.params['method'] = state_map.get(self.params.get('state'))
-
- def login(self):
- ''' Log in to APIC '''
-
- # Perform login request
- if self.params.get('port') is not None:
- url = '%(protocol)s://%(host)s:%(port)s/api/aaaLogin.json' % self.params
- else:
- url = '%(protocol)s://%(host)s/api/aaaLogin.json' % self.params
- payload = {'aaaUser': {'attributes': {'name': self.params.get('username'), 'pwd': self.params.get('password')}}}
- resp, auth = fetch_url(self.module, url,
- data=json.dumps(payload),
- method='POST',
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
-
- # Handle APIC response
- if auth.get('status') != 200:
- self.response = auth.get('msg')
- self.status = auth.get('status')
- try:
- # APIC error
- self.response_json(auth['body'])
- self.fail_json(msg='Authentication failed: %(code)s %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection failed for %(url)s. %(msg)s' % auth)
-
- # Retain cookie for later use
- self.headers['Cookie'] = resp.headers.get('Set-Cookie')
-
- def cert_auth(self, path=None, payload='', method=None):
- ''' Perform APIC signature-based authentication, not the expected SSL client certificate authentication. '''
-
- if method is None:
- method = self.params.get('method').upper()
-
- # NOTE: ACI documentation incorrectly uses complete URL
- if path is None:
- path = self.path
- path = '/' + path.lstrip('/')
-
- if payload is None:
- payload = ''
-
- # Check if we got a private key. This allows the use of vaulting the private key.
- if self.params.get('private_key').startswith('-----BEGIN PRIVATE KEY-----'):
- try:
- sig_key = load_privatekey(FILETYPE_PEM, self.params.get('private_key'))
- except Exception:
- self.module.fail_json(msg="Cannot load provided 'private_key' parameter.")
- # Use the username as the certificate_name value
- if self.params.get('certificate_name') is None:
- self.params['certificate_name'] = self.params.get('username')
- elif self.params.get('private_key').startswith('-----BEGIN CERTIFICATE-----'):
- self.module.fail_json(msg="Provided 'private_key' parameter value appears to be a certificate.")
- else:
- # If we got a private key file, read from this file.
- # NOTE: Avoid exposing any other credential as a filename in output...
- if not os.path.exists(self.params.get('private_key')):
- self.module.fail_json(msg="The provided private key file does not appear to exist. Is it a filename?")
- try:
- with open(self.params.get('private_key'), 'r') as fh:
- private_key_content = fh.read()
- except Exception:
- self.module.fail_json(msg="Cannot open private key file '%(private_key)s'." % self.params)
- if private_key_content.startswith('-----BEGIN PRIVATE KEY-----'):
- try:
- sig_key = load_privatekey(FILETYPE_PEM, private_key_content)
- except Exception:
- self.module.fail_json(msg="Cannot load private key file '%(private_key)s'." % self.params)
- # Use the private key basename (without extension) as certificate_name
- if self.params.get('certificate_name') is None:
- self.params['certificate_name'] = os.path.basename(os.path.splitext(self.params.get('private_key'))[0])
- elif private_key_content.startswith('-----BEGIN CERTIFICATE-----'):
- self.module.fail_json(msg="Provided private key file '%(private_key)s' appears to be a certificate." % self.params)
- else:
- self.module.fail_json(msg="Provided private key file '%(private_key)s' does not appear to be a private key." % self.params)
-
- # NOTE: ACI documentation incorrectly adds a space between method and path
- sig_request = method + path + payload
- sig_signature = base64.b64encode(sign(sig_key, sig_request, 'sha256'))
- sig_dn = 'uni/userext/user-%(username)s/usercert-%(certificate_name)s' % self.params
- self.headers['Cookie'] = 'APIC-Certificate-Algorithm=v1.0; ' +\
- 'APIC-Certificate-DN=%s; ' % sig_dn +\
- 'APIC-Certificate-Fingerprint=fingerprint; ' +\
- 'APIC-Request-Signature=%s' % to_native(sig_signature)
-
- def response_json(self, rawoutput):
- ''' Handle APIC JSON response output '''
- try:
- jsondata = json.loads(rawoutput)
- except Exception as e:
- # Expose RAW output for troubleshooting
- self.error = dict(code=-1, text="Unable to parse output as JSON, see 'raw' output. %s" % e)
- self.result['raw'] = rawoutput
- return
-
- # Extract JSON API output
- self.imdata = jsondata.get('imdata')
- if self.imdata is None:
- self.imdata = dict()
- self.totalCount = int(jsondata.get('totalCount'))
-
- # Handle possible APIC error information
- self.response_error()
-
- def response_xml(self, 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
- self.error = dict(code=-1, text="Unable to parse output as XML, see 'raw' output. %s" % e)
- self.result['raw'] = rawoutput
- return
-
- # Reformat as ACI does for JSON API output
- self.imdata = xmldata.get('imdata', {}).get('children')
- if self.imdata is None:
- self.imdata = dict()
- self.totalCount = int(xmldata.get('imdata', {}).get('attributes', {}).get('totalCount'))
-
- # Handle possible APIC error information
- self.response_error()
-
- def response_error(self):
- ''' Set error information when found '''
-
- # Handle possible APIC error information
- if self.totalCount != '0':
- try:
- self.error = self.imdata[0].get('error').get('attributes')
- except (AttributeError, IndexError, KeyError):
- pass
-
- def request(self, path, payload=None):
- ''' Perform a REST request '''
-
- # Ensure method is set (only do this once)
- self.define_method()
- self.path = path
-
- if self.params.get('port') is not None:
- self.url = '%(protocol)s://%(host)s:%(port)s/' % self.params + path.lstrip('/')
- else:
- self.url = '%(protocol)s://%(host)s/' % self.params + path.lstrip('/')
-
- # Sign and encode request as to APIC's wishes
- if self.params.get('private_key'):
- self.cert_auth(path=path, payload=payload)
-
- # Perform request
- resp, info = fetch_url(self.module, self.url,
- data=payload,
- headers=self.headers,
- method=self.params.get('method').upper(),
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
-
- self.response = info.get('msg')
- self.status = info.get('status')
-
- # Handle APIC response
- if info.get('status') != 200:
- try:
- # APIC error
- self.response_json(info['body'])
- self.fail_json(msg='APIC Error %(code)s: %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
-
- self.response_json(resp.read())
-
- def query(self, path):
- ''' Perform a query with no payload '''
-
- self.path = path
-
- if self.params.get('port') is not None:
- self.url = '%(protocol)s://%(host)s:%(port)s/' % self.params + path.lstrip('/')
- else:
- self.url = '%(protocol)s://%(host)s/' % self.params + path.lstrip('/')
-
- # Sign and encode request as to APIC's wishes
- if self.params.get('private_key'):
- self.cert_auth(path=path, method='GET')
-
- # Perform request
- resp, query = fetch_url(self.module, self.url,
- data=None,
- headers=self.headers,
- method='GET',
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
-
- # Handle APIC response
- if query.get('status') != 200:
- self.response = query.get('msg')
- self.status = query.get('status')
- try:
- # APIC error
- self.response_json(query['body'])
- self.fail_json(msg='APIC Error %(code)s: %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection failed for %(url)s. %(msg)s' % query)
-
- query = json.loads(resp.read())
-
- return json.dumps(query.get('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.get('diff', {}).get('before') != self.result.get('diff', {}).get('after'):
- self.result['changed'] = True
-
- # TODO: This could be designed to update existing keys
- def update_qs(self, params):
- ''' Append key-value pairs to self.filter_string '''
- accepted_params = dict((k, v) for (k, v) in params.items() if v is not None)
- if accepted_params:
- if self.filter_string:
- self.filter_string += '&'
- else:
- self.filter_string = '?'
- self.filter_string += '&'.join(['%s=%s' % (k, v) for (k, v) in accepted_params.items()])
-
- # TODO: This could be designed to accept multiple obj_classes and keys
- def build_filter(self, obj_class, params):
- ''' Build an APIC filter based on obj_class and key-value pairs '''
- accepted_params = dict((k, v) for (k, v) in params.items() if v is not None)
- if len(accepted_params) == 1:
- return ','.join('eq({0}.{1},"{2}")'.format(obj_class, k, v) for (k, v) in accepted_params.items())
- 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.get('target_class')
- target_filter = obj.get('target_filter')
- subtree_class = obj.get('subtree_class')
- subtree_filter = obj.get('subtree_filter')
- object_rn = obj.get('object_rn')
- mo = obj.get('module_object')
- add_subtree_filter = obj.get('add_subtree_filter')
- add_target_filter = obj.get('add_target_filter')
-
- if self.module.params.get('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 self.params.get('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.get('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.get('parent_class')
- target_class = target_object.get('aci_class')
- target_rn = target_object.get('aci_rn')
- target_filter = target_object.get('target_filter')
- target_module_object = target_object.get('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.get('parent_class')
- parent_class = parent_object.get('aci_class')
- parent_rn = parent_object.get('aci_rn')
- parent_filter = parent_object.get('target_filter')
- parent_module_object = parent_object.get('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.
-
- :param root_class: The top-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
- :param sublass_1: The second-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
- :param sublass_2: The third-level class dictionary containing aci_class, aci_rn, target_filter, and module_object keys.
- :param sublass_3: The fourth-level class dictionary containing 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 root_class: dict
- :type subclass_1: dict
- :type subclass_2: dict
- :type subclass_3: dict
- :type child_classes: list
- :return: The path and filter_string needed to build the full URL.
- """
- self.filter_string = ''
-
- if child_classes is None:
- self.child_classes = set()
- else:
- self.child_classes = set(child_classes)
-
- if subclass_3 is not None:
- self._construct_url_4(root_class, subclass_1, subclass_2, subclass_3)
- elif subclass_2 is not None:
- self._construct_url_3(root_class, subclass_1, subclass_2)
- elif subclass_1 is not None:
- self._construct_url_2(root_class, subclass_1)
- else:
- self._construct_url_1(root_class)
-
- if self.params.get('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:
- # Append child_classes to filter_string if filter string is empty
- self.update_qs({'rsp-subtree': 'full', 'rsp-subtree-class': ','.join(sorted(self.child_classes))})
-
- def _construct_url_1(self, obj):
- """
- This method is used by construct_url when the object is the top-level class.
- """
- obj_class = obj.get('aci_class')
- obj_rn = obj.get('aci_rn')
- obj_filter = obj.get('target_filter')
- mo = obj.get('module_object')
-
- if self.module.params.get('state') in ('absent', 'present'):
- # State is absent or present
- self.path = 'api/mo/uni/{0}.json'.format(obj_rn)
- self.update_qs({'rsp-prop-include': 'config-only'})
- elif mo is None:
- # Query for all objects of the module's class (filter by properties)
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- else:
- # Query for a specific object in the module's class
- self.path = 'api/mo/uni/{0}.json'.format(obj_rn)
-
- def _construct_url_2(self, parent, obj):
- """
- This method is used by construct_url when the object is the second-level class.
- """
- parent_class = parent.get('aci_class')
- parent_rn = parent.get('aci_rn')
- parent_filter = parent.get('target_filter')
- parent_obj = parent.get('module_object')
- obj_class = obj.get('aci_class')
- obj_rn = obj.get('aci_rn')
- obj_filter = obj.get('target_filter')
- mo = obj.get('module_object')
-
- if self.module.params.get('state') in ('absent', 'present'):
- # State is absent or present
- self.path = 'api/mo/uni/{0}/{1}.json'.format(parent_rn, obj_rn)
- self.update_qs({'rsp-prop-include': 'config-only'})
- elif parent_obj is None and mo is None:
- # Query for all objects of the module's class
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- elif parent_obj is None: # mo is known
- # Query for all objects of the module's class that match the provided ID value
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- elif mo is None: # parent_obj is known
- # Query for all object's of the module's class that belong to a specific parent object
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}.json'.format(parent_rn)
- else:
- # Query for specific object in the module's class
- self.path = 'api/mo/uni/{0}/{1}.json'.format(parent_rn, obj_rn)
-
- def _construct_url_3(self, root, parent, obj):
- """
- This method is used by construct_url when the object is the third-level class.
- """
- root_class = root.get('aci_class')
- root_rn = root.get('aci_rn')
- root_filter = root.get('target_filter')
- root_obj = root.get('module_object')
- parent_class = parent.get('aci_class')
- parent_rn = parent.get('aci_rn')
- parent_filter = parent.get('target_filter')
- parent_obj = parent.get('module_object')
- obj_class = obj.get('aci_class')
- obj_rn = obj.get('aci_rn')
- obj_filter = obj.get('target_filter')
- mo = obj.get('module_object')
-
- if self.module.params.get('state') in ('absent', 'present'):
- # State is absent or present
- self.path = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, parent_rn, obj_rn)
- self.update_qs({'rsp-prop-include': 'config-only'})
- elif root_obj is None and parent_obj is None and mo is None:
- # Query for all objects of the module's class
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- elif root_obj is None and parent_obj is None: # mo is known
- # Query for all objects of the module's class matching the provided ID value of the object
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- elif root_obj is None and mo is None: # parent_obj is known
- # Query for all objects of the module's class that belong to any parent class
- # matching the provided ID value for the parent object
- self.child_classes.add(obj_class)
- self.path = 'api/class/{0}.json'.format(parent_class)
- self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
- elif parent_obj is None and mo is None: # root_obj is known
- # Query for all objects of the module's class that belong to a specific root object
- self.child_classes.update([parent_class, obj_class])
- self.path = 'api/mo/uni/{0}.json'.format(root_rn)
- # NOTE: No need to select by root_filter
- # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
- elif root_obj is None: # mo and parent_obj are known
- # 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
- self.child_classes.add(obj_class)
- self.path = 'api/class/{0}.json'.format(parent_class)
- self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
- self.update_qs({'rsp-subtree-filter': self.build_filter(obj_class, obj_filter)})
- elif parent_obj is None: # mo and root_obj are known
- # Query for all objects of the module's class that match the provided ID value and belong to a specific root object
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}.json'.format(root_rn)
- # NOTE: No need to select by root_filter
- # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
- # TODO: Filter by parent_filter and obj_filter
- self.update_qs({'rsp-subtree-filter': self.build_filter(obj_class, obj_filter)})
- elif mo is None: # root_obj and parent_obj are known
- # Query for all objects of the module's class that belong to a specific parent object
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}/{1}.json'.format(root_rn, parent_rn)
- # NOTE: No need to select by parent_filter
- # self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
- else:
- # Query for a specific object of the module's class
- self.path = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, parent_rn, obj_rn)
-
- def _construct_url_4(self, root, sec, parent, obj):
- """
- This method is used by construct_url when the object is the fourth-level class.
- """
- root_class = root.get('aci_class')
- root_rn = root.get('aci_rn')
- root_filter = root.get('target_filter')
- root_obj = root.get('module_object')
- sec_class = sec.get('aci_class')
- sec_rn = sec.get('aci_rn')
- sec_filter = sec.get('target_filter')
- sec_obj = sec.get('module_object')
- parent_class = parent.get('aci_class')
- parent_rn = parent.get('aci_rn')
- parent_filter = parent.get('target_filter')
- parent_obj = parent.get('module_object')
- obj_class = obj.get('aci_class')
- obj_rn = obj.get('aci_rn')
- obj_filter = obj.get('target_filter')
- mo = obj.get('module_object')
-
- if self.child_classes is None:
- self.child_classes = [obj_class]
-
- if self.module.params.get('state') in ('absent', 'present'):
- # State is absent or present
- self.path = 'api/mo/uni/{0}/{1}/{2}/{3}.json'.format(root_rn, sec_rn, parent_rn, obj_rn)
- self.update_qs({'rsp-prop-include': 'config-only'})
- # TODO: Add all missing cases
- elif root_obj is None:
- self.child_classes.add(obj_class)
- self.path = 'api/class/{0}.json'.format(obj_class)
- self.update_qs({'query-target-filter': self.build_filter(obj_class, obj_filter)})
- elif sec_obj is None:
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}.json'.format(root_rn)
- # NOTE: No need to select by root_filter
- # self.update_qs({'query-target-filter': self.build_filter(root_class, root_filter)})
- # TODO: Filter by sec_filter, parent and obj_filter
- self.update_qs({'rsp-subtree-filter': self.build_filter(obj_class, obj_filter)})
- elif parent_obj is None:
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}/{1}.json'.format(root_rn, sec_rn)
- # NOTE: No need to select by sec_filter
- # self.update_qs({'query-target-filter': self.build_filter(sec_class, sec_filter)})
- # TODO: Filter by parent_filter and obj_filter
- self.update_qs({'rsp-subtree-filter': self.build_filter(obj_class, obj_filter)})
- elif mo is None:
- self.child_classes.add(obj_class)
- self.path = 'api/mo/uni/{0}/{1}/{2}.json'.format(root_rn, sec_rn, parent_rn)
- # NOTE: No need to select by parent_filter
- # self.update_qs({'query-target-filter': self.build_filter(parent_class, parent_filter)})
- else:
- # Query for a specific object of the module's class
- self.path = 'api/mo/uni/{0}/{1}/{2}/{3}.json'.format(root_rn, sec_rn, parent_rn, obj_rn)
-
- 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.proposed = dict()
-
- if not self.existing:
- return
-
- elif not self.module.check_mode:
- # Sign and encode request as to APIC's wishes
- if self.params['private_key']:
- self.cert_auth(method='DELETE')
-
- resp, info = fetch_url(self.module, self.url,
- headers=self.headers,
- method='DELETE',
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
-
- self.response = info.get('msg')
- self.status = info.get('status')
- self.method = 'DELETE'
-
- # Handle APIC response
- if info.get('status') == 200:
- self.result['changed'] = True
- self.response_json(resp.read())
- else:
- try:
- # APIC error
- self.response_json(info['body'])
- self.fail_json(msg='APIC Error %(code)s: %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
- else:
- self.result['changed'] = True
- self.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.proposed[aci_class]['attributes']
- if self.existing:
- existing_config = self.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.get('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.proposed
-
- self.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():
- existing_field = existing_child.get(key)
- if value != existing_field:
- 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.proposed[aci_class].get('children')
- if proposed_children:
- child_updates = []
- existing_children = self.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.url + self.filter_string
-
- # Sign and encode request as to APIC's wishes
- if self.params.get('private_key'):
- self.cert_auth(path=self.path + self.filter_string, method='GET')
-
- resp, info = fetch_url(self.module, uri,
- headers=self.headers,
- method='GET',
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
- self.response = info.get('msg')
- self.status = info.get('status')
- self.method = 'GET'
-
- # Handle APIC response
- if info.get('status') == 200:
- self.existing = json.loads(resp.read())['imdata']
- else:
- try:
- # APIC error
- self.response_json(info['body'])
- self.fail_json(msg='APIC Error %(code)s: %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection 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
-
- # FIXME: Design causes issues for repeated child_classes
- # 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']
- # NOTE: This is an ugly fix
- # Return the one that is a subset match
- if set(proposed_config.items()).issubset(set(existing_config.items())):
- 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.proposed = {aci_class: {'attributes': proposed}}
-
- # add child objects to proposed
- if child_configs:
- children = []
- for child in child_configs:
- child_copy = deepcopy(child)
- has_value = False
- for root_key in child_copy.keys():
- for final_keys, values in child_copy[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.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.config:
- return
- elif not self.module.check_mode:
- # Sign and encode request as to APIC's wishes
- if self.params.get('private_key'):
- self.cert_auth(method='POST', payload=json.dumps(self.config))
-
- resp, info = fetch_url(self.module, self.url,
- data=json.dumps(self.config),
- headers=self.headers,
- method='POST',
- timeout=self.params.get('timeout'),
- use_proxy=self.params.get('use_proxy'))
-
- self.response = info.get('msg')
- self.status = info.get('status')
- self.method = 'POST'
-
- # Handle APIC response
- if info.get('status') == 200:
- self.result['changed'] = True
- self.response_json(resp.read())
- else:
- try:
- # APIC error
- self.response_json(info['body'])
- self.fail_json(msg='APIC Error %(code)s: %(text)s' % self.error)
- except KeyError:
- # Connection error
- self.fail_json(msg='Connection failed for %(url)s. %(msg)s' % info)
- else:
- self.result['changed'] = True
- self.method = 'POST'
-
- def exit_json(self, **kwargs):
-
- if 'state' in self.params:
- if self.params.get('state') in ('absent', 'present'):
- if self.params.get('output_level') in ('debug', 'info'):
- self.result['previous'] = self.existing
-
- # Return the gory details when we need it
- if self.params.get('output_level') == 'debug':
- if 'state' in self.params:
- self.result['filter_string'] = self.filter_string
- self.result['method'] = self.method
- # self.result['path'] = self.path # Adding 'path' in result causes state: absent in output
- self.result['response'] = self.response
- self.result['status'] = self.status
- self.result['url'] = self.url
-
- if 'state' in self.params:
- self.original = self.existing
- if self.params.get('state') in ('absent', 'present'):
- self.get_existing()
-
- # if self.module._diff and self.original != self.existing:
- # self.result['diff'] = dict(
- # before=json.dumps(self.original, sort_keys=True, indent=4),
- # after=json.dumps(self.existing, sort_keys=True, indent=4),
- # )
- self.result['current'] = self.existing
-
- if self.params.get('output_level') in ('debug', 'info'):
- self.result['sent'] = self.config
- self.result['proposed'] = self.proposed
-
- self.result.update(**kwargs)
- self.module.exit_json(**self.result)
-
- def fail_json(self, msg, **kwargs):
-
- # Return error information, if we have it
- if self.error.get('code') is not None and self.error.get('text') is not None:
- self.result['error'] = self.error
-
- if 'state' in self.params:
- if self.params.get('state') in ('absent', 'present'):
- if self.params.get('output_level') in ('debug', 'info'):
- self.result['previous'] = self.existing
-
- # Return the gory details when we need it
- if self.params.get('output_level') == 'debug':
- if self.imdata is not None:
- self.result['imdata'] = self.imdata
- self.result['totalCount'] = self.totalCount
-
- if self.params.get('output_level') == 'debug':
- if self.url is not None:
- if 'state' in self.params:
- self.result['filter_string'] = self.filter_string
- self.result['method'] = self.method
- # self.result['path'] = self.path # Adding 'path' in result causes state: absent in output
- self.result['response'] = self.response
- self.result['status'] = self.status
- self.result['url'] = self.url
-
- if 'state' in self.params:
- if self.params.get('output_level') in ('debug', 'info'):
- self.result['sent'] = self.config
- self.result['proposed'] = self.proposed
-
- self.result.update(**kwargs)
- self.module.fail_json(msg=msg, **self.result)