# 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. # # (c) 2018 Red Hat Inc. # # 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 os from functools import partial from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_text try: from infoblox_client.connector import Connector from infoblox_client.exceptions import InfobloxException HAS_INFOBLOX_CLIENT = True except ImportError: HAS_INFOBLOX_CLIENT = False nios_provider_spec = { 'host': dict(), 'username': dict(), 'password': dict(no_log=True), 'ssl_verify': dict(type='bool', default=False), 'silent_ssl_warnings': dict(type='bool', default=True), 'http_request_timeout': dict(type='int', default=10), 'http_pool_connections': dict(type='int', default=10), 'http_pool_maxsize': dict(type='int', default=10), 'max_retries': dict(type='int', default=3), 'wapi_version': dict(default='1.4'), 'max_results': dict(type='int', default=1000) } def get_connector(*args, **kwargs): ''' Returns an instance of infoblox_client.connector.Connector :params args: positional arguments are silently ignored :params kwargs: dict that is passed to Connector init :returns: Connector ''' if not HAS_INFOBLOX_CLIENT: raise Exception('infoblox-client is required but does not appear ' 'to be installed. It can be installed using the ' 'command `pip install infoblox-client`') if not set(kwargs.keys()).issubset(nios_provider_spec.keys()): raise Exception('invalid or unsupported keyword argument for connector') for key, value in iteritems(nios_provider_spec): if key not in kwargs: # apply default values from nios_provider_spec since we cannot just # assume the provider values are coming from AnsibleModule if 'default' in value: kwargs[key] = value['default'] # override any values with env variables unless they were # explicitly set env = ('INFOBLOX_%s' % key).upper() if env in os.environ: kwargs[key] = os.environ.get(env) return Connector(kwargs) def normalize_extattrs(value): ''' Normalize extattrs field to expected format The module accepts extattrs as key/value pairs. This method will transform the key/value pairs into a structure suitable for sending across WAPI in the format of: extattrs: { key: { value: } } ''' return dict([(k, {'value': v}) for k, v in iteritems(value)]) def flatten_extattrs(value): ''' Flatten the key/value struct for extattrs WAPI returns extattrs field as a dict in form of: extattrs: { key: { value: } } This method will flatten the structure to: extattrs: { key: value } ''' return dict([(k, v['value']) for k, v in iteritems(value)]) class WapiBase(object): ''' Base class for implementing Infoblox WAPI API ''' provider_spec = {'provider': dict(type='dict', options=nios_provider_spec)} def __init__(self, provider): self.connector = get_connector(**provider) def __getattr__(self, name): try: return self.__dict__[name] except KeyError: if name.startswith('_'): raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) return partial(self._invoke_method, name) def _invoke_method(self, name, *args, **kwargs): try: method = getattr(self.connector, name) return method(*args, **kwargs) except InfobloxException as exc: if hasattr(self, 'handle_exception'): self.handle_exception(name, exc) else: raise class WapiLookup(WapiBase): ''' Implements WapiBase for lookup plugins ''' pass class WapiInventory(WapiBase): ''' Implements WapiBase for dynamic inventory script ''' pass class WapiModule(WapiBase): ''' Implements WapiBase for executing a NIOS module ''' def __init__(self, module): self.module = module provider = module.params['provider'] try: super(WapiModule, self).__init__(provider) except Exception as exc: self.module.fail_json(msg=to_text(exc)) def handle_exception(self, method_name, exc): ''' Handles any exceptions raised This method will be called if an InfobloxException is raised for any call to the instance of Connector. This method will then gracefully fail the module. :args exc: instance of InfobloxException ''' self.module.fail_json( msg=exc.response['text'], type=exc.response['Error'].split(':')[0], code=exc.response.get('code'), operation=method_name ) def run(self, ib_obj_type, ib_spec): ''' Runs the module and performans configuration tasks :args ib_obj_type: the WAPI object type to operate against :args ib_spec: the specification for the WAPI object as a dict :returns: a results dict ''' state = self.module.params['state'] if state not in ('present', 'absent'): self.module.fail_json(msg='state must be one of `present`, `absent`, got `%s`' % state) result = {'changed': False} obj_filter = dict([(k, self.module.params[k]) for k, v in iteritems(ib_spec) if v.get('ib_req')]) ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=ib_spec.keys()) if ib_obj: current_object = ib_obj[0] if 'extattrs' in current_object: current_object['extattrs'] = flatten_extattrs(current_object['extattrs']) ref = current_object.pop('_ref') else: current_object = obj_filter ref = None proposed_object = {} for key, value in iteritems(ib_spec): if self.module.params[key] is not None: if 'transform' in value: proposed_object[key] = value['transform'](self.module) else: proposed_object[key] = self.module.params[key] modified = not self.compare_objects(current_object, proposed_object) if 'extattrs' in proposed_object: proposed_object['extattrs'] = normalize_extattrs(proposed_object['extattrs']) if state == 'present': if ref is None: if not self.module.check_mode: self.create_object(ib_obj_type, proposed_object) result['changed'] = True elif modified: if 'network_view' in proposed_object: self.check_if_network_view_exists(proposed_object['network_view']) proposed_object.pop('network_view') elif 'view' in proposed_object: self.check_if_dns_view_exists(proposed_object['view']) if not self.module.check_mode: res = self.update_object(ref, proposed_object) result['changed'] = True elif state == 'absent': if ref is not None: if not self.module.check_mode: self.delete_object(ref) result['changed'] = True return result def check_if_dns_view_exists(self, name, fail_on_missing=True): ''' Checks if the specified DNS view is already configured :args name: the name of the DNS view to check :args fail_on_missing: fail the module if the DNS view does not exist :returns: True if the network_view exists and False if the DNS view does not exist and fail_on_missing is False ''' res = self.get_object('view', {'name': name}) is not None if not res and fail_on_missing: self.module.fail_json(msg='DNS view %s does not exist, please create ' 'it using nios_dns_view first' % name) return res def check_if_network_view_exists(self, name, fail_on_missing=True): ''' Checks if the specified network_view is already configured :args name: the name of the network view to check :args fail_on_missing: fail the module if the network_view does not exist :returns: True if the network_view exists and False if the network_view does not exist and fail_on_missing is False ''' res = self.get_object('networkview', {'name': name}) is not None if not res and fail_on_missing: self.module.fail_json(msg='Network view %s does not exist, please create ' 'it using nios_network_view first' % name) return res def issubset(self, item, objects): ''' Checks if item is a subset of objects :args item: the subset item to validate :args objects: superset list of objects to validate against :returns: True if item is a subset of one entry in objects otherwise this method will return None ''' for obj in objects: if isinstance(item, dict): if all(entry in obj.items() for entry in item.items()): return True else: if item in obj: return True def compare_objects(self, current_object, proposed_object): for key, proposed_item in iteritems(proposed_object): current_item = current_object.get(key) # if proposed has a key that current doesn't then the objects are # not equal and False will be immediately returned if current_item is None: return False elif isinstance(proposed_item, list): for subitem in proposed_item: if not self.issubset(subitem, current_item): return False elif isinstance(proposed_item, dict): return self.compare_objects(current_item, proposed_item) else: if current_item != proposed_item: return False return True