1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/module_utils/network/avi/ansible_utils.py

573 lines
21 KiB
Python
Raw Normal View History

2020-03-09 10:11:07 +01:00
from __future__ import absolute_import
"""
Created on Aug 16, 2016
@author: Gaurav Rastogi (grastogi@avinetworks.com)
"""
import os
import re
import logging
import sys
from copy import deepcopy
from ansible.module_utils.basic import env_fallback
try:
from ansible_collections.community.general.plugins.module_utils.network.avi.avi_api import (
ApiSession, ObjectNotFound, avi_sdk_syslog_logger, AviCredentials, HAS_AVI)
except ImportError:
HAS_AVI = False
if os.environ.get('AVI_LOG_HANDLER', '') != 'syslog':
log = logging.getLogger(__name__)
else:
# Ansible does not allow logging from the modules.
log = avi_sdk_syslog_logger()
def _check_type_string(x):
"""
:param x:
:return: True if it is of type string
"""
if isinstance(x, str):
return True
if sys.version_info[0] < 3:
try:
return isinstance(x, unicode)
except NameError:
return False
class AviCheckModeResponse(object):
"""
Class to support ansible check mode.
"""
def __init__(self, obj, status_code=200):
self.obj = obj
self.status_code = status_code
def json(self):
return self.obj
def ansible_return(module, rsp, changed, req=None, existing_obj=None,
api_context=None):
"""
:param module: AnsibleModule
:param rsp: ApiResponse from avi_api
:param changed: boolean
:param req: ApiRequest to avi_api
:param existing_obj: object to be passed debug output
:param api_context: api login context
helper function to return the right ansible based on the error code and
changed
Returns: specific ansible module exit function
"""
if rsp is not None and rsp.status_code > 299:
return module.fail_json(
msg='Error %d Msg %s req: %s api_context:%s ' % (
rsp.status_code, rsp.text, req, api_context))
api_creds = AviCredentials()
api_creds.update_from_ansible_module(module)
key = '%s:%s:%s' % (api_creds.controller, api_creds.username,
api_creds.port)
disable_fact = module.params.get('avi_disable_session_cache_as_fact')
fact_context = None
if not disable_fact:
fact_context = module.params.get('api_context', {})
if fact_context:
fact_context.update({key: api_context})
else:
fact_context = {key: api_context}
obj_val = rsp.json() if rsp else existing_obj
if (obj_val and module.params.get("obj_username", None) and
"username" in obj_val):
obj_val["obj_username"] = obj_val["username"]
if (obj_val and module.params.get("obj_password", None) and
"password" in obj_val):
obj_val["obj_password"] = obj_val["password"]
old_obj_val = existing_obj if changed and existing_obj else None
api_context_val = api_context if disable_fact else None
ansible_facts_val = dict(
avi_api_context=fact_context) if not disable_fact else {}
return module.exit_json(
changed=changed, obj=obj_val, old_obj=old_obj_val,
ansible_facts=ansible_facts_val, api_context=api_context_val)
def purge_optional_fields(obj, module):
"""
It purges the optional arguments to be sent to the controller.
:param obj: dictionary of the ansible object passed as argument.
:param module: AnsibleModule
return modified obj
"""
purge_fields = []
for param, spec in module.argument_spec.items():
if not spec.get('required', False):
if param not in obj:
# these are ansible common items
continue
if obj[param] is None:
purge_fields.append(param)
log.debug('purging fields %s', purge_fields)
for param in purge_fields:
obj.pop(param, None)
return obj
def cleanup_absent_fields(obj):
"""
cleans up any field that is marked as state: absent. It needs to be removed
from the object if it is present.
:param obj:
:return: Purged object
"""
if type(obj) != dict:
return obj
cleanup_keys = []
for k, v in obj.items():
if type(v) == dict:
if (('state' in v and v['state'] == 'absent') or
(v == "{'state': 'absent'}")):
cleanup_keys.append(k)
else:
cleanup_absent_fields(v)
if not v:
cleanup_keys.append(k)
elif type(v) == list:
new_list = []
for elem in v:
elem = cleanup_absent_fields(elem)
if elem:
# remove the item from list
new_list.append(elem)
if new_list:
obj[k] = new_list
else:
cleanup_keys.append(k)
elif isinstance(v, str) or isinstance(v, str):
if v == "{'state': 'absent'}":
cleanup_keys.append(k)
for k in cleanup_keys:
del obj[k]
return obj
RE_REF_MATCH = re.compile(r'^/api/[\w/]+\?name\=[\w]+[^#<>]*$')
# if HTTP ref match then strip out the #name
HTTP_REF_MATCH = re.compile(r'https://[\w.0-9:-]+/api/.+')
HTTP_REF_W_NAME_MATCH = re.compile(r'https://[\w.0-9:-]+/api/.*#.+')
def ref_n_str_cmp(x, y):
"""
compares two references
1. check for exact reference
2. check for obj_type/uuid
3. check for name
if x is ref=name then extract uuid and name from y and use it.
if x is http_ref then
strip x and y
compare them.
if x and y are urls then match with split on #
if x is a RE_REF_MATCH then extract name
if y is a REF_MATCH then extract name
:param x: first string
:param y: second string from controller's object
Returns
True if they are equivalent else False
"""
if type(y) in (int, float, bool, int, complex):
y = str(y)
x = str(x)
if not (_check_type_string(x) and _check_type_string(y)):
return False
y_uuid = y_name = str(y)
x = str(x)
if RE_REF_MATCH.match(x):
x = x.split('name=')[1]
elif HTTP_REF_MATCH.match(x):
x = x.rsplit('#', 1)[0]
y = y.rsplit('#', 1)[0]
elif RE_REF_MATCH.match(y):
y = y.split('name=')[1]
if HTTP_REF_W_NAME_MATCH.match(y):
path = y.split('api/', 1)[1]
# Fetching name or uuid from path /xxxx_xx/xx/xx_x/uuid_or_name
uuid_or_name = path.split('/')[-1]
parts = uuid_or_name.rsplit('#', 1)
y_uuid = parts[0]
y_name = parts[1] if len(parts) > 1 else ''
# is just string but y is a url so match either uuid or name
result = (x in (y, y_name, y_uuid))
if not result:
log.debug('x: %s y: %s y_name %s y_uuid %s',
x, y, y_name, y_uuid)
return result
def avi_obj_cmp(x, y, sensitive_fields=None):
"""
compares whether x is fully contained in y. The comparision is different
from a simple dictionary compare for following reasons
1. Some fields could be references. The object in controller returns the
full URL for those references. However, the ansible script would have
it specified as /api/pool?name=blah. So, the reference fields need
to match uuid, relative reference based on name and actual reference.
2. Optional fields with defaults: In case there are optional fields with
defaults then controller automatically fills it up. This would
cause the comparison with Ansible object specification to always return
changed.
3. Optional fields without defaults: This is most tricky. The issue is
how to specify deletion of such objects from ansible script. If the
ansible playbook has object specified as Null then Avi controller will
reject for non Message(dict) type fields. In addition, to deal with the
defaults=null issue all the fields that are set with None are purged
out before comparing with Avi controller's version
So, the solution is to pass state: absent if any optional field needs
to be deleted from the configuration. The script would return changed
=true if it finds a key in the controller version and it is marked with
state: absent in ansible playbook. Alternatively, it would return
false if key is not present in the controller object. Before, doing
put or post it would purge the fields that are marked state: absent.
:param x: first string
:param y: second string from controller's object
:param sensitive_fields: sensitive fields to ignore for diff
Returns:
True if x is subset of y else False
"""
if not sensitive_fields:
sensitive_fields = set()
if isinstance(x, str) or isinstance(x, str):
# Special handling for strings as they can be references.
return ref_n_str_cmp(x, y)
if type(x) not in [list, dict]:
# if it is not list or dict or string then simply compare the values
return x == y
if type(x) == list:
# should compare each item in the list and that should match
if len(x) != len(y):
log.debug('x has %d items y has %d', len(x), len(y))
return False
for i in zip(x, y):
if not avi_obj_cmp(i[0], i[1], sensitive_fields=sensitive_fields):
# no need to continue
return False
if type(x) == dict:
x.pop('_last_modified', None)
x.pop('tenant', None)
y.pop('_last_modified', None)
x.pop('api_version', None)
y.pop('api_verison', None)
d_xks = [k for k in x.keys() if k in sensitive_fields]
if d_xks:
# if there is sensitive field then always return changed
return False
# pop the keys that are marked deleted but not present in y
# return false if item is marked absent and is present in y
d_x_absent_ks = []
for k, v in x.items():
if v is None:
d_x_absent_ks.append(k)
continue
if isinstance(v, dict):
if ('state' in v) and (v['state'] == 'absent'):
if type(y) == dict and k not in y:
d_x_absent_ks.append(k)
else:
return False
elif not v:
d_x_absent_ks.append(k)
elif isinstance(v, list) and not v:
d_x_absent_ks.append(k)
# Added condition to check key in dict.
elif isinstance(v, str) or (k in y and isinstance(y[k], str)):
# this is the case when ansible converts the dictionary into a
# string.
if v == "{'state': 'absent'}" and k not in y:
d_x_absent_ks.append(k)
elif not v and k not in y:
# this is the case when x has set the value that qualifies
# as not but y does not have that value
d_x_absent_ks.append(k)
for k in d_x_absent_ks:
x.pop(k)
x_keys = set(x.keys())
y_keys = set(y.keys())
if not x_keys.issubset(y_keys):
# log.debug('x has %s and y has %s keys', len(x_keys), len(y_keys))
return False
for k, v in x.items():
if k not in y:
# log.debug('k %s is not in y %s', k, y)
return False
if not avi_obj_cmp(v, y[k], sensitive_fields=sensitive_fields):
# log.debug('k %s v %s did not match in y %s', k, v, y[k])
return False
return True
POP_FIELDS = ['state', 'controller', 'username', 'password', 'api_version',
'avi_credentials', 'avi_api_update_method', 'avi_api_patch_op',
'api_context', 'tenant', 'tenant_uuid', 'avi_disable_session_cache_as_fact']
def get_api_context(module, api_creds):
api_context = module.params.get('api_context')
if api_context and module.params.get('avi_disable_session_cache_as_fact'):
return api_context
elif api_context and not module.params.get(
'avi_disable_session_cache_as_fact'):
key = '%s:%s:%s' % (api_creds.controller, api_creds.username,
api_creds.port)
return api_context.get(key)
else:
return None
def avi_ansible_api(module, obj_type, sensitive_fields):
"""
This converts the Ansible module into AVI object and invokes APIs
:param module: Ansible module
:param obj_type: string representing Avi object type
:param sensitive_fields: sensitive fields to be excluded for comparison
purposes.
Returns:
success: module.exit_json with obj=avi object
faliure: module.fail_json
"""
api_creds = AviCredentials()
api_creds.update_from_ansible_module(module)
api_context = get_api_context(module, api_creds)
if api_context:
api = ApiSession.get_session(
api_creds.controller,
api_creds.username,
password=api_creds.password,
timeout=api_creds.timeout,
tenant=api_creds.tenant,
tenant_uuid=api_creds.tenant_uuid,
token=api_context['csrftoken'],
port=api_creds.port,
session_id=api_context['session_id'],
csrftoken=api_context['csrftoken'])
else:
api = ApiSession.get_session(
api_creds.controller,
api_creds.username,
password=api_creds.password,
timeout=api_creds.timeout,
tenant=api_creds.tenant,
tenant_uuid=api_creds.tenant_uuid,
token=api_creds.token,
port=api_creds.port)
state = module.params['state']
# Get the api version.
avi_update_method = module.params.get('avi_api_update_method', 'put')
avi_patch_op = module.params.get('avi_api_patch_op', 'add')
api_version = api_creds.api_version
name = module.params.get('name', None)
# Added Support to get uuid
uuid = module.params.get('uuid', None)
check_mode = module.check_mode
if uuid and obj_type != 'cluster':
obj_path = '%s/%s' % (obj_type, uuid)
else:
obj_path = '%s/' % obj_type
obj = deepcopy(module.params)
tenant = obj.pop('tenant', '')
tenant_uuid = obj.pop('tenant_uuid', '')
# obj.pop('cloud_ref', None)
for k in POP_FIELDS:
obj.pop(k, None)
purge_optional_fields(obj, module)
# Special code to handle situation where object has a field
# named username. This is used in case of api/user
# The following code copies the username and password
# from the obj_username and obj_password fields.
if 'obj_username' in obj:
obj['username'] = obj['obj_username']
obj.pop('obj_username')
if 'obj_password' in obj:
obj['password'] = obj['obj_password']
obj.pop('obj_password')
if 'full_name' not in obj and 'name' in obj and obj_type == "user":
obj['full_name'] = obj['name']
# Special case as name represent full_name in user module
# As per API response, name is always same as username regardless of full_name
obj['name'] = obj['username']
log.info('passed object %s ', obj)
if uuid:
# Get the object based on uuid.
try:
existing_obj = api.get(
obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
params={'include_refs': '', 'include_name': ''},
api_version=api_version)
existing_obj = existing_obj.json()
except ObjectNotFound:
existing_obj = None
elif name:
params = {'include_refs': '', 'include_name': ''}
if obj.get('cloud_ref', None):
# this is the case when gets have to be scoped with cloud
cloud = obj['cloud_ref'].split('name=')[1]
params['cloud_ref.name'] = cloud
existing_obj = api.get_object_by_name(
obj_type, name, tenant=tenant, tenant_uuid=tenant_uuid,
params=params, api_version=api_version)
# Need to check if tenant_ref was provided and the object returned
# is actually in admin tenant.
if existing_obj and 'tenant_ref' in obj and 'tenant_ref' in existing_obj:
# https://10.10.25.42/api/tenant/admin#admin
existing_obj_tenant = existing_obj['tenant_ref'].split('#')[1]
obj_tenant = obj['tenant_ref'].split('name=')[1]
if obj_tenant != existing_obj_tenant:
existing_obj = None
else:
# added api version to avi api call.
existing_obj = api.get(obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
params={'include_refs': '', 'include_name': ''},
api_version=api_version).json()
if state == 'absent':
rsp = None
changed = False
err = False
if not check_mode and existing_obj:
try:
if name is not None:
# added api version to avi api call.
rsp = api.delete_by_name(
obj_type, name, tenant=tenant, tenant_uuid=tenant_uuid,
api_version=api_version)
else:
# added api version to avi api call.
rsp = api.delete(
obj_path, tenant=tenant, tenant_uuid=tenant_uuid,
api_version=api_version)
except ObjectNotFound:
pass
if check_mode and existing_obj:
changed = True
if rsp:
if rsp.status_code == 204:
changed = True
else:
err = True
if not err:
return ansible_return(
module, rsp, changed, existing_obj=existing_obj,
api_context=api.get_context())
elif rsp:
return module.fail_json(msg=rsp.text)
rsp = None
req = None
if existing_obj:
# this is case of modify as object exists. should find out
# if changed is true or not
if name is not None and obj_type != 'cluster':
obj_uuid = existing_obj['uuid']
obj_path = '%s/%s' % (obj_type, obj_uuid)
if avi_update_method == 'put':
changed = not avi_obj_cmp(obj, existing_obj, sensitive_fields)
obj = cleanup_absent_fields(obj)
if changed:
req = obj
if check_mode:
# No need to process any further.
rsp = AviCheckModeResponse(obj=existing_obj)
else:
rsp = api.put(
obj_path, data=req, tenant=tenant,
tenant_uuid=tenant_uuid, api_version=api_version)
elif check_mode:
rsp = AviCheckModeResponse(obj=existing_obj)
else:
if check_mode:
# No need to process any further.
rsp = AviCheckModeResponse(obj=existing_obj)
changed = True
else:
obj.pop('name', None)
patch_data = {avi_patch_op: obj}
rsp = api.patch(
obj_path, data=patch_data, tenant=tenant,
tenant_uuid=tenant_uuid, api_version=api_version)
obj = rsp.json()
changed = not avi_obj_cmp(obj, existing_obj)
if changed:
log.debug('EXISTING OBJ %s', existing_obj)
log.debug('NEW OBJ %s', obj)
else:
changed = True
req = obj
if check_mode:
rsp = AviCheckModeResponse(obj=None)
else:
rsp = api.post(obj_type, data=obj, tenant=tenant,
tenant_uuid=tenant_uuid, api_version=api_version)
return ansible_return(module, rsp, changed, req, existing_obj=existing_obj,
api_context=api.get_context())
def avi_common_argument_spec():
"""
Returns common arguments for all Avi modules
:return: dict
"""
credentials_spec = dict(
controller=dict(fallback=(env_fallback, ['AVI_CONTROLLER'])),
username=dict(fallback=(env_fallback, ['AVI_USERNAME'])),
password=dict(fallback=(env_fallback, ['AVI_PASSWORD']), no_log=True),
api_version=dict(default='16.4.4', type='str'),
tenant=dict(default='admin'),
tenant_uuid=dict(default='', type='str'),
port=dict(type='int'),
timeout=dict(default=300, type='int'),
token=dict(default='', type='str', no_log=True),
session_id=dict(default='', type='str', no_log=True),
csrftoken=dict(default='', type='str', no_log=True)
)
return dict(
controller=dict(fallback=(env_fallback, ['AVI_CONTROLLER'])),
username=dict(fallback=(env_fallback, ['AVI_USERNAME'])),
password=dict(fallback=(env_fallback, ['AVI_PASSWORD']), no_log=True),
tenant=dict(default='admin'),
tenant_uuid=dict(default=''),
api_version=dict(default='16.4.4', type='str'),
avi_credentials=dict(default=None, type='dict',
options=credentials_spec),
api_context=dict(type='dict'),
avi_disable_session_cache_as_fact=dict(default=False, type='bool'))