mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 11:21:25 -07:00
* refactors nios api shared code to handle provider better This change refactors the shared code to be easily shared between modules, plugins and dynamic inventory scripts. All parts now implement the provider arguments uniformly. This also provides a centralized fix to suppress urllib3 warnings coming from the requests library implemented by infoblox_client * fix up pep8 errors * fix missing var name
322 lines
12 KiB
Python
322 lines
12 KiB
Python
# 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')
|
|
}
|
|
|
|
|
|
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: <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: <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
|