1132 lines
43 KiB
Python
1132 lines
43 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
|
|
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
from copy import deepcopy
|
|
import re
|
|
import os
|
|
import ast
|
|
import datetime
|
|
import shutil
|
|
import tempfile
|
|
from ansible.module_utils.basic import json
|
|
from ansible.module_utils.basic import env_fallback
|
|
from ansible.module_utils.six import PY3
|
|
from ansible.module_utils.six.moves import filterfalse
|
|
from ansible.module_utils.six.moves.urllib.parse import urlencode, urljoin
|
|
from ansible.module_utils.urls import fetch_url
|
|
from ansible.module_utils._text import to_native, to_text
|
|
from ansible.module_utils.connection import Connection
|
|
try:
|
|
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
HAS_MULTIPART_ENCODER = True
|
|
except ImportError:
|
|
HAS_MULTIPART_ENCODER = False
|
|
|
|
|
|
if PY3:
|
|
def cmp(a, b):
|
|
return (a > b) - (a < b)
|
|
|
|
|
|
def issubset(subset, superset):
|
|
''' Recurse through nested dictionary and compare entries '''
|
|
|
|
# Both objects are the same object
|
|
if subset is superset:
|
|
return True
|
|
|
|
# Both objects are identical
|
|
if subset == superset:
|
|
return True
|
|
|
|
# Both objects have a different type
|
|
if type(subset) != type(superset):
|
|
return False
|
|
|
|
for key, value in subset.items():
|
|
# Ignore empty values
|
|
if value is None:
|
|
return True
|
|
|
|
# Item from subset is missing from superset
|
|
if key not in superset:
|
|
return False
|
|
|
|
# Item has different types in subset and superset
|
|
if type(superset.get(key)) != type(value):
|
|
return False
|
|
|
|
# Compare if item values are subset
|
|
if isinstance(value, dict):
|
|
if not issubset(superset.get(key), value):
|
|
return False
|
|
elif isinstance(value, list):
|
|
try:
|
|
# NOTE: Fails for lists of dicts
|
|
if not set(value) <= set(superset.get(key)):
|
|
return False
|
|
except TypeError:
|
|
# Fall back to exact comparison for lists of dicts
|
|
diff = list(filterfalse(lambda i: i in value, superset.get(key))) + list(filterfalse(lambda j: j in superset.get(key), value))
|
|
if diff:
|
|
return False
|
|
elif isinstance(value, set):
|
|
if not value <= superset.get(key):
|
|
return False
|
|
else:
|
|
if not value == superset.get(key):
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def update_qs(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)
|
|
return '?' + urlencode(accepted_params)
|
|
|
|
|
|
def mso_argument_spec():
|
|
return dict(
|
|
host=dict(type='str', required=False, aliases=['hostname'], fallback=(env_fallback, ['MSO_HOST'])),
|
|
port=dict(type='int', required=False, fallback=(env_fallback, ['MSO_PORT'])),
|
|
username=dict(type='str', required=False, fallback=(env_fallback, ['MSO_USERNAME', 'ANSIBLE_NET_USERNAME'])),
|
|
password=dict(type='str', required=False, no_log=True, fallback=(env_fallback, ['MSO_PASSWORD', 'ANSIBLE_NET_PASSWORD'])),
|
|
output_level=dict(type='str', default='normal', choices=['debug', 'info', 'normal'], fallback=(env_fallback, ['MSO_OUTPUT_LEVEL'])),
|
|
timeout=dict(type='int', default=30, fallback=(env_fallback, ['MSO_TIMEOUT'])),
|
|
use_proxy=dict(type='bool', fallback=(env_fallback, ['MSO_USE_PROXY'])),
|
|
use_ssl=dict(type='bool', fallback=(env_fallback, ['MSO_USE_SSL'])),
|
|
validate_certs=dict(type='bool', fallback=(env_fallback, ['MSO_VALIDATE_CERTS'])),
|
|
login_domain=dict(type='str', fallback=(env_fallback, ['MSO_LOGIN_DOMAIN'])),
|
|
)
|
|
|
|
|
|
def mso_reference_spec():
|
|
return dict(
|
|
name=dict(type='str', required=True),
|
|
schema=dict(type='str'),
|
|
template=dict(type='str'),
|
|
)
|
|
|
|
|
|
def mso_epg_subnet_spec():
|
|
return dict(
|
|
subnet=dict(type='str', required=True, aliases=['ip']),
|
|
description=dict(type='str'),
|
|
scope=dict(type='str', default='private', choices=['private', 'public']),
|
|
shared=dict(type='bool', default=False),
|
|
no_default_gateway=dict(type='bool', default=False),
|
|
)
|
|
|
|
|
|
def mso_subnet_spec():
|
|
subnet_spec = mso_epg_subnet_spec()
|
|
subnet_spec.update(dict(querier=dict(type='bool', default=False)))
|
|
return subnet_spec
|
|
|
|
|
|
def mso_bd_subnet_spec():
|
|
subnet_spec = mso_epg_subnet_spec()
|
|
subnet_spec.update(dict(querier=dict(type='bool', default=False)))
|
|
subnet_spec.update(dict(primary=dict(type='bool', default=False)))
|
|
subnet_spec.update(dict(virtual=dict(type='bool', default=False)))
|
|
return subnet_spec
|
|
|
|
|
|
def mso_dhcp_spec():
|
|
return dict(
|
|
dhcp_option_policy=dict(type='dict', options=mso_dhcp_option_spec()),
|
|
name=dict(type='str', required=True),
|
|
version=dict(type='int', required=True),
|
|
)
|
|
|
|
|
|
def mso_dhcp_option_spec():
|
|
return dict(
|
|
name=dict(type='str', required=True),
|
|
version=dict(type='int', required=True),
|
|
)
|
|
|
|
|
|
def mso_contractref_spec():
|
|
return dict(
|
|
name=dict(type='str', required=True),
|
|
schema=dict(type='str'),
|
|
template=dict(type='str'),
|
|
type=dict(type='str', required=True, choices=['consumer', 'provider']),
|
|
)
|
|
|
|
|
|
def mso_expression_spec():
|
|
return dict(
|
|
type=dict(type='str', required=True, aliases=['tag']),
|
|
operator=dict(type='str', choices=['not_in', 'in', 'equals', 'not_equals', 'has_key', 'does_not_have_key'], required=True),
|
|
value=dict(type='str'),
|
|
)
|
|
|
|
|
|
def mso_expression_spec_ext_epg():
|
|
return dict(
|
|
type=dict(type='str', choices=['ip_address'], required=True),
|
|
operator=dict(type='str', choices=['equals'], required=True),
|
|
value=dict(type='str', required=True),
|
|
)
|
|
|
|
|
|
def mso_hub_network_spec():
|
|
return dict(
|
|
name=dict(type='str', required=True),
|
|
tenant=dict(type='str', required=True),
|
|
)
|
|
|
|
|
|
def mso_object_migrate_spec():
|
|
return dict(
|
|
epg=dict(type='str', required=True),
|
|
anp=dict(type='str', required=True),
|
|
)
|
|
|
|
|
|
# Copied from ansible's module uri.py (url): https://github.com/ansible/ansible/blob/cdf62edc65f564fff6b7e575e084026fa7faa409/lib/ansible/modules/uri.py
|
|
def write_file(module, url, dest, content, resp):
|
|
# create a tempfile with some test content
|
|
fd, tmpsrc = tempfile.mkstemp(dir=module.tmpdir)
|
|
f = open(tmpsrc, 'wb')
|
|
try:
|
|
f.write(content)
|
|
except Exception as e:
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Failed to create temporary content file: {0}".format(to_native(e)))
|
|
f.close()
|
|
|
|
checksum_src = None
|
|
checksum_dest = None
|
|
|
|
# raise an error if there is no tmpsrc file
|
|
if not os.path.exists(tmpsrc):
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Source '{0}' does not exist".format(tmpsrc))
|
|
if not os.access(tmpsrc, os.R_OK):
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Source '{0}' is not readable".format(tmpsrc))
|
|
checksum_src = module.sha1(tmpsrc)
|
|
|
|
# check if there is no dest file
|
|
if os.path.exists(dest):
|
|
# raise an error if copy has no permission on dest
|
|
if not os.access(dest, os.W_OK):
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Destination '{0}' not writable".format(dest))
|
|
if not os.access(dest, os.R_OK):
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Destination '{0}' not readable".format(dest))
|
|
checksum_dest = module.sha1(dest)
|
|
else:
|
|
if not os.access(os.path.dirname(dest), os.W_OK):
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="Destination dir '{0}' not writable".format(os.path.dirname(dest)))
|
|
|
|
if checksum_src != checksum_dest:
|
|
try:
|
|
shutil.copyfile(tmpsrc, dest)
|
|
except Exception as e:
|
|
os.remove(tmpsrc)
|
|
module.fail_json(msg="failed to copy {0} to {1}: {2}".format(tmpsrc, dest, to_native(e)))
|
|
|
|
os.remove(tmpsrc)
|
|
|
|
|
|
class MSOModule(object):
|
|
|
|
def __init__(self, module):
|
|
self.module = module
|
|
self.params = module.params
|
|
self.result = dict(changed=False)
|
|
self.headers = {'Content-Type': 'text/json'}
|
|
self.platform = "mso"
|
|
|
|
# normal output
|
|
self.existing = dict()
|
|
|
|
# mso_rest output
|
|
self.jsondata = None
|
|
self.error = dict(code=None, message=None, info=None)
|
|
|
|
# info output
|
|
self.previous = dict()
|
|
self.proposed = dict()
|
|
self.sent = dict()
|
|
self.stdout = None
|
|
|
|
# debug output
|
|
self.has_modified = False
|
|
self.filter_string = ''
|
|
self.method = None
|
|
self.path = None
|
|
self.response = None
|
|
self.status = None
|
|
self.url = None
|
|
|
|
if self.module._debug:
|
|
self.module.warn('Enable debug output because ANSIBLE_DEBUG was set.')
|
|
self.params['output_level'] = 'debug'
|
|
|
|
if self.module._socket_path is None:
|
|
if self.params.get('use_ssl') is None:
|
|
self.params['use_ssl'] = True
|
|
if self.params.get('use_proxy') is None:
|
|
self.params['use_proxy'] = True
|
|
if self.params.get('validate_certs') is None:
|
|
self.params['validate_certs'] = True
|
|
|
|
# Ensure protocol is set
|
|
self.params['protocol'] = 'https' if self.params.get('use_ssl', True) else 'http'
|
|
|
|
# Set base_uri
|
|
if self.params.get('port') is not None:
|
|
self.base_only_uri = '{protocol}://{host}:{port}/'.format(**self.params)
|
|
self.baseuri = '{0}api/v1/'.format(self.base_only_uri)
|
|
else:
|
|
self.base_only_uri = '{protocol}://{host}/'.format(**self.params)
|
|
self.baseuri = '{0}api/v1/'.format(self.base_only_uri)
|
|
|
|
if self.params.get('host') is None:
|
|
self.fail_json(msg="Parameter 'host' is required when not using the HTTP API connection plugin")
|
|
|
|
if self.params.get('password'):
|
|
# Perform password-based authentication, log on using password
|
|
self.login()
|
|
else:
|
|
self.fail_json(msg="Parameter 'password' is required for authentication")
|
|
else:
|
|
self.connection = Connection(self.module._socket_path)
|
|
if self.connection.get_platform() == "cisco.nd":
|
|
self.platform = "nd"
|
|
|
|
def get_login_domain_id(self, domain):
|
|
''' Get a domain and return its id '''
|
|
if domain is None:
|
|
return domain
|
|
d = self.get_obj('auth/login-domains', key='domains', name=domain)
|
|
if not d:
|
|
self.fail_json(msg="Login domain '%s' is not a valid domain name." % domain)
|
|
if 'id' not in d:
|
|
self.fail_json(msg="Login domain lookup failed for domain '%s': %s" % (domain, d))
|
|
return d['id']
|
|
|
|
def login(self):
|
|
''' Log in to MSO '''
|
|
|
|
# Perform login request
|
|
if (self.params.get('login_domain') is not None) and (self.params.get('login_domain') != 'Local'):
|
|
domain_id = self.get_login_domain_id(self.params.get('login_domain'))
|
|
payload = {'username': self.params.get('username', 'admin'), 'password': self.params.get('password'), 'domainId': domain_id}
|
|
else:
|
|
payload = {'username': self.params.get('username', 'admin'), 'password': self.params.get('password')}
|
|
self.url = urljoin(self.baseuri, 'auth/login')
|
|
resp, auth = fetch_url(self.module,
|
|
self.url,
|
|
data=json.dumps(payload),
|
|
method='POST',
|
|
headers=self.headers,
|
|
timeout=self.params.get('timeout'),
|
|
use_proxy=self.params.get('use_proxy'))
|
|
|
|
# Handle MSO response
|
|
if auth.get('status') not in [200, 201]:
|
|
self.response = auth.get('msg')
|
|
self.status = auth.get('status')
|
|
self.fail_json(msg='Authentication failed: {msg}'.format(**auth))
|
|
|
|
payload = json.loads(resp.read())
|
|
|
|
self.headers['Authorization'] = 'Bearer {token}'.format(**payload)
|
|
|
|
def response_json(self, rawoutput):
|
|
''' Handle MSO JSON response output '''
|
|
try:
|
|
self.jsondata = json.loads(rawoutput)
|
|
except Exception as e:
|
|
# Expose RAW output for troubleshooting
|
|
self.error = dict(code=-1, message="Unable to parse output as JSON, see 'raw' output. %s" % e)
|
|
self.result['raw'] = rawoutput
|
|
return
|
|
|
|
# Handle possible MSO error information
|
|
if self.status not in [200, 201, 202, 204]:
|
|
self.error = self.jsondata
|
|
|
|
def request_download(self, path, destination=None):
|
|
self.url = urljoin(self.baseuri, path)
|
|
redirected = False
|
|
redir_info = {}
|
|
redirect = {}
|
|
|
|
src = self.params.get('src')
|
|
if src:
|
|
try:
|
|
self.headers.update({
|
|
'Content-Length': os.stat(src).st_size
|
|
})
|
|
data = open(src, 'rb')
|
|
except OSError:
|
|
self.fail_json(msg='Unable to open source file %s' % src, elapsed=0)
|
|
else:
|
|
pass
|
|
|
|
data = None
|
|
|
|
kwargs = {}
|
|
if destination is not None:
|
|
if os.path.isdir(destination):
|
|
# first check if we are redirected to a file download
|
|
check, redir_info = fetch_url(self.module, self.url,
|
|
headers=self.headers,
|
|
method='GET',
|
|
timeout=self.params.get('timeout'))
|
|
# if we are redirected, update the url with the location header,
|
|
# and update dest with the new url filename
|
|
if redir_info['status'] in (301, 302, 303, 307):
|
|
self.url = redir_info.get('location')
|
|
redirected = True
|
|
destination = os.path.join(destination, check.headers.get("Content-Disposition").split("filename=")[1])
|
|
# if destination file already exist, only download if file newer
|
|
if os.path.exists(destination):
|
|
kwargs['last_mod_time'] = datetime.datetime.utcfromtimestamp(os.path.getmtime(destination))
|
|
|
|
resp, info = fetch_url(self.module, self.url, data=data, headers=self.headers,
|
|
method='GET', timeout=self.params.get('timeout'), unix_socket=self.params.get('unix_socket'), **kwargs)
|
|
|
|
try:
|
|
content = resp.read()
|
|
except AttributeError:
|
|
# there was no content, but the error read() may have been stored in the info as 'body'
|
|
content = info.pop('body', '')
|
|
|
|
if src:
|
|
# Try to close the open file handle
|
|
try:
|
|
data.close()
|
|
except Exception:
|
|
pass
|
|
|
|
redirect['redirected'] = redirected or info.get('url') != self.url
|
|
redirect.update(redir_info)
|
|
redirect.update(info)
|
|
|
|
write_file(self.module, self.url, destination, content, redirect)
|
|
|
|
return redirect, destination
|
|
|
|
def request_upload(self, path, fields=None):
|
|
''' Generic HTTP MultiPart POST method for MSO uploads. '''
|
|
self.path = path
|
|
self.url = urljoin(self.baseuri, path)
|
|
|
|
if not HAS_MULTIPART_ENCODER:
|
|
self.fail_json(msg='requests-toolbelt is required for the upload state of this module')
|
|
|
|
mp_encoder = MultipartEncoder(fields=fields)
|
|
self.headers['Content-Type'] = mp_encoder.content_type
|
|
self.headers['Accept-Encoding'] = "gzip, deflate, br"
|
|
|
|
resp, info = fetch_url(self.module,
|
|
self.url,
|
|
headers=self.headers,
|
|
data=mp_encoder,
|
|
method='POST',
|
|
timeout=self.params.get('timeout'),
|
|
use_proxy=self.params.get('use_proxy'))
|
|
|
|
self.response = info.get('msg')
|
|
self.status = info.get('status')
|
|
|
|
# Get change status from HTTP headers
|
|
if 'modified' in info:
|
|
self.has_modified = True
|
|
if info.get('modified') == 'false':
|
|
self.result['changed'] = False
|
|
elif info.get('modified') == 'true':
|
|
self.result['changed'] = True
|
|
|
|
# 200: OK, 201: Created, 202: Accepted, 204: No Content
|
|
if self.status in (200, 201, 202, 204):
|
|
output = resp.read()
|
|
if output:
|
|
return json.loads(output)
|
|
|
|
# 400: Bad Request, 401: Unauthorized, 403: Forbidden,
|
|
# 405: Method Not Allowed, 406: Not Acceptable
|
|
# 500: Internal Server Error, 501: Not Implemented
|
|
elif self.status >= 400:
|
|
try:
|
|
payload = json.loads(resp.read())
|
|
except (ValueError, AttributeError):
|
|
try:
|
|
payload = json.loads(info.get('body'))
|
|
except Exception:
|
|
self.fail_json(msg='MSO Error:', info=info)
|
|
if 'code' in payload:
|
|
self.fail_json(msg='MSO Error {code}: {message}'.format(**payload), info=info, payload=payload)
|
|
else:
|
|
self.fail_json(msg='MSO Error:'.format(**payload), info=info, payload=payload)
|
|
|
|
return {}
|
|
|
|
def request(self, path, method=None, data=None, qs=None, api_version="v1"):
|
|
''' Generic HTTP method for MSO requests. '''
|
|
self.path = path
|
|
|
|
if method is not None:
|
|
self.method = method
|
|
|
|
# If we PATCH with empty operations, return
|
|
if method == 'PATCH' and not data:
|
|
return {}
|
|
|
|
# if method in ['PATCH', 'PUT']:
|
|
# if qs is not None:
|
|
# qs['enableVersionCheck'] = 'true'
|
|
# else:
|
|
# qs = dict(enableVersionCheck='true')
|
|
|
|
if method in ['PATCH']:
|
|
if qs is not None:
|
|
qs['validate'] = 'false'
|
|
else:
|
|
qs = dict(validate='false')
|
|
|
|
resp = None
|
|
if self.module._socket_path:
|
|
self.connection.set_params(self.params)
|
|
if api_version is not None:
|
|
uri = '/mso/api/{0}/{1}'.format(api_version, self.path)
|
|
else:
|
|
uri = self.path
|
|
|
|
if qs is not None:
|
|
uri = uri + update_qs(qs)
|
|
|
|
try:
|
|
info = self.connection.send_request(method, uri, json.dumps(data))
|
|
self.url = info.get('url')
|
|
info.pop('date')
|
|
except Exception as e:
|
|
try:
|
|
error_obj = json.loads(to_text(e))
|
|
except Exception:
|
|
error_obj = dict(error=dict(
|
|
code=-1,
|
|
message="Unable to parse error output as JSON. Raw error message: {0}".format(e),
|
|
exception=to_text(e)
|
|
))
|
|
pass
|
|
self.fail_json(msg=error_obj['error']['message'])
|
|
|
|
else:
|
|
if api_version is not None:
|
|
self.url = '{0}api/{1}/{2}'.format(self.base_only_uri, api_version, self.path.lstrip('/'))
|
|
else:
|
|
self.url = '{0}{1}'.format(self.base_only_uri, self.path.lstrip('/'))
|
|
|
|
if qs is not None:
|
|
self.url = self.url + update_qs(qs)
|
|
resp, info = fetch_url(self.module,
|
|
self.url,
|
|
headers=self.headers,
|
|
data=json.dumps(data),
|
|
method=self.method,
|
|
timeout=self.params.get('timeout'),
|
|
use_proxy=self.params.get('use_proxy'))
|
|
|
|
self.response = info.get('msg')
|
|
self.status = info.get('status', -1)
|
|
|
|
# Get change status from HTTP headers
|
|
if 'modified' in info:
|
|
self.has_modified = True
|
|
if info.get('modified') == 'false':
|
|
self.result['changed'] = False
|
|
elif info.get('modified') == 'true':
|
|
self.result['changed'] = True
|
|
|
|
# 200: OK, 201: Created, 202: Accepted
|
|
if self.status in (200, 201, 202):
|
|
try:
|
|
output = resp.read()
|
|
if output:
|
|
try:
|
|
return json.loads(output)
|
|
except Exception as e:
|
|
self.error = dict(code=-1, message="Unable to parse output as JSON, see 'raw' output. {0}".format(e))
|
|
self.result['raw'] = output
|
|
return
|
|
except AttributeError:
|
|
return info.get('body')
|
|
|
|
# 204: No Content
|
|
elif self.status == 204:
|
|
return {}
|
|
|
|
# 404: Not Found
|
|
elif self.method == 'DELETE' and self.status == 404:
|
|
return {}
|
|
|
|
# 400: Bad Request, 401: Unauthorized, 403: Forbidden,
|
|
# 405: Method Not Allowed, 406: Not Acceptable
|
|
# 500: Internal Server Error, 501: Not Implemented
|
|
elif self.status >= 400:
|
|
self.result['status'] = self.status
|
|
body = info.get('body')
|
|
if body is not None:
|
|
try:
|
|
if isinstance(body, dict):
|
|
payload = body
|
|
else:
|
|
payload = json.loads(body)
|
|
except Exception as e:
|
|
self.error = dict(code=-1, message="Unable to parse output as JSON, see 'raw' output. %s" % e)
|
|
self.result['raw'] = body
|
|
self.fail_json(msg='MSO Error:', data=data, info=info)
|
|
self.error = payload
|
|
if 'code' in payload:
|
|
self.fail_json(msg='MSO Error {code}: {message}'.format(**payload), data=data, info=info, payload=payload)
|
|
else:
|
|
self.fail_json(msg='MSO Error:'.format(**payload), data=data, info=info, payload=payload)
|
|
else:
|
|
# Connection error
|
|
msg = 'Connection failed for {0}. {1}'.format(info.get('url'), info.get('msg'))
|
|
self.error = msg
|
|
self.fail_json(msg=msg)
|
|
return {}
|
|
|
|
def query_objs(self, path, key=None, api_version='v1', **kwargs):
|
|
''' Query the MSO REST API for objects in a path '''
|
|
found = []
|
|
objs = self.request(path, api_version=api_version, method='GET')
|
|
|
|
if objs == {} or objs == []:
|
|
return found
|
|
|
|
if key is None:
|
|
key = path
|
|
|
|
if isinstance(objs, dict):
|
|
if key not in objs:
|
|
self.fail_json(msg="Key '{0}' missing from data".format(key), data=objs)
|
|
objs_list = objs.get(key)
|
|
else:
|
|
objs_list = objs
|
|
for obj in objs_list:
|
|
for kw_key, kw_value in kwargs.items():
|
|
if kw_value is None:
|
|
continue
|
|
if isinstance(kw_value, dict):
|
|
obj_value = obj.get(kw_key)
|
|
if obj_value is not None and isinstance(obj_value, dict):
|
|
breakout = False
|
|
for kw_key_lvl2, kw_value_lvl2 in kw_value.items():
|
|
if obj_value.get(kw_key_lvl2) != kw_value_lvl2:
|
|
breakout = True
|
|
break
|
|
if breakout:
|
|
break
|
|
else:
|
|
break
|
|
elif obj.get(kw_key) != kw_value:
|
|
break
|
|
else:
|
|
found.append(obj)
|
|
|
|
return found
|
|
|
|
def query_obj(self, path, api_version='v1', **kwargs):
|
|
''' Query the MSO REST API for the whole object at a path '''
|
|
obj = self.request(path, api_version=api_version, method='GET')
|
|
if obj == {}:
|
|
return {}
|
|
for kw_key, kw_value in kwargs.items():
|
|
if kw_value is None:
|
|
continue
|
|
if isinstance(kw_value, dict):
|
|
obj_value = obj.get(kw_key)
|
|
if obj_value is not None and isinstance(obj_value, dict):
|
|
for kw_key_lvl2, kw_value_lvl2 in kw_value.items():
|
|
if obj_value.get(kw_key_lvl2) != kw_value_lvl2:
|
|
return {}
|
|
elif obj.get(kw_key) != kw_value:
|
|
return {}
|
|
return obj
|
|
|
|
def get_obj(self, path, api_version='v1', **kwargs):
|
|
''' Get a specific object from a set of MSO REST objects '''
|
|
objs = self.query_objs(path, api_version=api_version, **kwargs)
|
|
if len(objs) == 0:
|
|
return {}
|
|
if len(objs) > 1:
|
|
self.fail_json(msg='More than one object matches unique filter: {0}'.format(kwargs))
|
|
return objs[0]
|
|
|
|
def lookup_schema(self, schema):
|
|
''' Look up schema and return its id '''
|
|
if schema is None:
|
|
return schema
|
|
|
|
schema_summary = self.query_objs('schemas/list-identity', key='schemas', displayName=schema)
|
|
if not schema_summary:
|
|
self.fail_json(msg="Provided schema '{0}' does not exist.".format(schema))
|
|
schema_id = schema_summary[0].get('id')
|
|
if not schema_id:
|
|
self.fail_json(msg="Schema lookup failed for schema '{0}': '{1}'".format(schema, schema_id))
|
|
return schema_id
|
|
|
|
def lookup_domain(self, domain):
|
|
''' Look up a domain and return its id '''
|
|
if domain is None:
|
|
return domain
|
|
|
|
d = self.get_obj('auth/domains', key='domains', name=domain)
|
|
if not d:
|
|
self.fail_json(msg="Domain '%s' is not a valid domain name." % domain)
|
|
if 'id' not in d:
|
|
self.fail_json(msg="Domain lookup failed for domain '%s': %s" % (domain, d))
|
|
return d.get('id')
|
|
|
|
def lookup_roles(self, roles):
|
|
''' Look up roles and return their ids '''
|
|
if roles is None:
|
|
return roles
|
|
|
|
ids = []
|
|
for role in roles:
|
|
access_type = "readWrite"
|
|
try:
|
|
role = ast.literal_eval(role)
|
|
if type(role) is dict and 'name' in role:
|
|
name = role.get('name')
|
|
if role.get('access_type') == 'read':
|
|
access_type = 'readOnly'
|
|
except ValueError:
|
|
name = role
|
|
|
|
r = self.get_obj('roles', name=name)
|
|
if not r:
|
|
self.fail_json(msg="Role '%s' is not a valid role name." % name)
|
|
if 'id' not in r:
|
|
self.fail_json(msg="Role lookup failed for role '%s': %s" % (name, r))
|
|
ids.append(dict(roleId=r.get('id'), accessType=access_type))
|
|
return ids
|
|
|
|
def lookup_site(self, site):
|
|
''' Look up a site and return its id '''
|
|
if site is None:
|
|
return site
|
|
|
|
s = self.get_obj('sites', name=site)
|
|
if not s:
|
|
self.fail_json(msg="Site '%s' is not a valid site name." % site)
|
|
if 'id' not in s:
|
|
self.fail_json(msg="Site lookup failed for site '%s': %s" % (site, s))
|
|
return s.get('id')
|
|
|
|
def lookup_sites(self, sites):
|
|
''' Look up sites and return their ids '''
|
|
if sites is None:
|
|
return sites
|
|
|
|
ids = []
|
|
for site in sites:
|
|
s = self.get_obj('sites', name=site)
|
|
if not s:
|
|
self.fail_json(msg="Site '%s' is not a valid site name." % site)
|
|
if 'id' not in s:
|
|
self.fail_json(msg="Site lookup failed for site '%s': %s" % (site, s))
|
|
ids.append(dict(siteId=s.get('id'), securityDomains=[]))
|
|
return ids
|
|
|
|
def lookup_tenant(self, tenant):
|
|
''' Look up a tenant and return its id '''
|
|
if tenant is None:
|
|
return tenant
|
|
|
|
t = self.get_obj('tenants', key='tenants', name=tenant)
|
|
if not t:
|
|
self.fail_json(msg="Tenant '%s' is not valid tenant name." % tenant)
|
|
if 'id' not in t:
|
|
self.fail_json(msg="Tenant lookup failed for tenant '%s': %s" % (tenant, t))
|
|
return t.get('id')
|
|
|
|
def lookup_remote_location(self, remote_location):
|
|
''' Look up a remote location and return its path and id '''
|
|
if remote_location is None:
|
|
return None
|
|
|
|
remote = self.get_obj('platform/remote-locations', key='remoteLocations', name=remote_location)
|
|
if 'id' not in remote:
|
|
self.fail_json(msg="No remote location found for remote '%s'" % (remote_location))
|
|
remote_info = dict(id=remote.get('id'), path=remote.get('credential')['remotePath'])
|
|
return remote_info
|
|
|
|
def lookup_users(self, users):
|
|
''' Look up users and return their ids '''
|
|
# Ensure tenant has at least admin user
|
|
if users is None:
|
|
users = ['admin']
|
|
elif 'admin' not in users:
|
|
users.append('admin')
|
|
|
|
ids = []
|
|
for user in users:
|
|
if self.platform == "nd":
|
|
u = self.get_obj('users', loginID=user, api_version='v2')
|
|
else:
|
|
u = self.get_obj('users', username=user)
|
|
if not u:
|
|
self.fail_json(msg="User '%s' is not a valid user name." % user)
|
|
if 'id' not in u:
|
|
if 'userID' not in u:
|
|
self.fail_json(msg="User lookup failed for user '%s': %s" % (user, u))
|
|
id = dict(userId=u.get('userID'))
|
|
else:
|
|
id = dict(userId=u.get('id'))
|
|
if id in ids:
|
|
self.fail_json(msg="User '%s' is duplicate." % user)
|
|
ids.append(id)
|
|
|
|
return ids
|
|
|
|
def create_label(self, label, label_type):
|
|
''' Create a new label '''
|
|
return self.request('labels', method='POST', data=dict(displayName=label, type=label_type))
|
|
|
|
def lookup_labels(self, labels, label_type):
|
|
''' Look up labels and return their ids (create if necessary) '''
|
|
if labels is None:
|
|
return None
|
|
|
|
ids = []
|
|
for label in labels:
|
|
label_obj = self.get_obj('labels', displayName=label)
|
|
if not label_obj:
|
|
label_obj = self.create_label(label, label_type)
|
|
if 'id' not in label_obj:
|
|
self.fail_json(msg="Label lookup failed for label '%s': %s" % (label, label_obj))
|
|
ids.append(label_obj.get('id'))
|
|
return ids
|
|
|
|
def anp_ref(self, **data):
|
|
''' Create anpRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/anps/{anp}'.format(**data)
|
|
|
|
def epg_ref(self, **data):
|
|
''' Create epgRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/anps/{anp}/epgs/{epg}'.format(**data)
|
|
|
|
def bd_ref(self, **data):
|
|
''' Create bdRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/bds/{bd}'.format(**data)
|
|
|
|
def contract_ref(self, **data):
|
|
''' Create contractRef string '''
|
|
# Support the contract argspec
|
|
if 'name' in data:
|
|
data['contract'] = data.get('name')
|
|
return '/schemas/{schema_id}/templates/{template}/contracts/{contract}'.format(**data)
|
|
|
|
def filter_ref(self, **data):
|
|
''' Create a filterRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/filters/{filter}'.format(**data)
|
|
|
|
def vrf_ref(self, **data):
|
|
''' Create vrfRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/vrfs/{vrf}'.format(**data)
|
|
|
|
def l3out_ref(self, **data):
|
|
''' Create l3outRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/l3outs/{l3out}'.format(**data)
|
|
|
|
def ext_epg_ref(self, **data):
|
|
''' Create extEpgRef string '''
|
|
return '/schemas/{schema_id}/templates/{template}/externalEpgs/{external_epg}'.format(**data)
|
|
|
|
def vrf_dict_from_ref(self, data):
|
|
vrf_ref_regex = re.compile(r'\/schemas\/(.*)\/templates\/(.*)\/vrfs\/(.*)')
|
|
vrf_dict = vrf_ref_regex.search(data)
|
|
return {
|
|
'vrfName': vrf_dict.group(3),
|
|
'schemaId': vrf_dict.group(1),
|
|
'templateName': vrf_dict.group(2),
|
|
}
|
|
|
|
def dict_from_ref(self, data):
|
|
if data and data != '':
|
|
ref_regex = re.compile(r'\/schemas\/(.*)\/templates\/(.*?)\/(.*?)\/(.*)')
|
|
dic = ref_regex.search(data)
|
|
if dic is not None:
|
|
schema_id = dic.group(1)
|
|
template_name = dic.group(2)
|
|
category = dic.group(3)
|
|
name = dic.group(4)
|
|
uri_map = {
|
|
'vrfs': ['vrfName', 'schemaId', 'templateName'],
|
|
'bds': ['bdName', 'schemaId', 'templateName'],
|
|
'filters': ['filterName', 'schemaId', 'templateName'],
|
|
'contracts': ['contractName', 'schemaId', 'templateName'],
|
|
'l3outs': ['l3outName', 'schemaId', 'templateName'],
|
|
'anps': ['anpName', 'schemaId', 'templateName'],
|
|
}
|
|
result = {
|
|
uri_map[category][1]: schema_id,
|
|
uri_map[category][2]: template_name,
|
|
}
|
|
|
|
self.recursive_dict_from_ref_regex(name, result, uri_map[category][0])
|
|
|
|
return result
|
|
else:
|
|
self.fail_json(msg="There was no group in search: {data}".format(data=data))
|
|
|
|
def recursive_dict_from_ref_regex(self, data, result, category):
|
|
continued_ref_regex = re.compile(r'(.*?)\/([a-zA-Z]+.*)')
|
|
section_ref_regex = re.compile(r'([a-zA-Z]+)\/(.*)')
|
|
dic_name = continued_ref_regex.search(data)
|
|
if dic_name is not None:
|
|
result[category] = dic_name.group(1)
|
|
next_section = dic_name.group(2)
|
|
dic_next_section = section_ref_regex.search(next_section)
|
|
if dic_next_section is not None:
|
|
next_name = dic_next_section.group(2)
|
|
self.recursive_dict_from_ref_regex(next_name, result, dic_next_section.group(1).rstrip("s") + "Name")
|
|
else:
|
|
result[category] = data
|
|
|
|
def recursive_dict_from_ref(self, data):
|
|
for key in data:
|
|
if key.endswith('Ref'):
|
|
data[key] = self.dict_from_ref(data.get(key))
|
|
if isinstance(data[key], list):
|
|
for item in data[key]:
|
|
self.recursive_dict_from_ref(item)
|
|
return data
|
|
|
|
def make_reference(self, data, reftype, schema_id, template):
|
|
''' Create a reference from a dictionary '''
|
|
# Removes entry from payload
|
|
if data is None:
|
|
return None
|
|
|
|
if data.get('schema') is not None:
|
|
schema_obj = self.get_obj('schemas', displayName=data.get('schema'))
|
|
if not schema_obj:
|
|
self.fail_json(msg="Referenced schema '{schema}' in {reftype}ref does not exist".format(reftype=reftype, **data))
|
|
schema_id = schema_obj.get('id')
|
|
|
|
if data.get('template') is not None:
|
|
template = data.get('template')
|
|
|
|
refname = '%sName' % reftype
|
|
|
|
return {
|
|
refname: data.get('name'),
|
|
'schemaId': schema_id,
|
|
'templateName': template,
|
|
}
|
|
|
|
def make_subnets(self, data, is_bd_subnet=True):
|
|
''' Create a subnets list from input '''
|
|
if data is None:
|
|
return None
|
|
|
|
subnets = []
|
|
for subnet in data:
|
|
if 'subnet' in subnet:
|
|
subnet['ip'] = subnet.get('subnet')
|
|
if subnet.get('description') is None:
|
|
subnet['description'] = subnet.get('subnet')
|
|
subnet_payload = dict(
|
|
ip=subnet.get('ip'),
|
|
description=str(subnet.get('description')),
|
|
scope=subnet.get('scope'),
|
|
shared=subnet.get('shared'),
|
|
noDefaultGateway=subnet.get('no_default_gateway'),
|
|
)
|
|
if is_bd_subnet:
|
|
subnet_payload.update(dict(querier=subnet.get('querier'), primary=subnet.get('primary'), virtual=subnet.get('virtual')))
|
|
subnets.append(subnet_payload)
|
|
|
|
return subnets
|
|
|
|
def make_dhcp_label(self, data):
|
|
''' Create a DHCP policy from input '''
|
|
if data is None:
|
|
return None
|
|
if type(data) == list:
|
|
dhcps = []
|
|
for dhcp in data:
|
|
if 'dhcp_option_policy' in dhcp:
|
|
dhcp['dhcpOptionLabel'] = dhcp.get('dhcp_option_policy')
|
|
del dhcp['dhcp_option_policy']
|
|
dhcps.append(dhcp)
|
|
return dhcps
|
|
if 'version' in data:
|
|
data['version'] = int(data.get('version'))
|
|
if data and 'dhcp_option_policy' in data:
|
|
dhcp_option_policy = data.get('dhcp_option_policy')
|
|
if dhcp_option_policy is not None and 'version' in dhcp_option_policy:
|
|
dhcp_option_policy['version'] = int(dhcp_option_policy.get('version'))
|
|
data['dhcpOptionLabel'] = dhcp_option_policy
|
|
del data['dhcp_option_policy']
|
|
return data
|
|
|
|
def sanitize(self, updates, collate=False, required=None, unwanted=None):
|
|
''' Clean up unset keys from a request payload '''
|
|
if required is None:
|
|
required = []
|
|
if unwanted is None:
|
|
unwanted = []
|
|
self.proposed = deepcopy(self.existing)
|
|
self.sent = deepcopy(self.existing)
|
|
|
|
for key in self.existing:
|
|
# Remove References
|
|
if key.endswith('Ref'):
|
|
del(self.proposed[key])
|
|
del(self.sent[key])
|
|
continue
|
|
|
|
# Removed unwanted keys
|
|
elif key in unwanted:
|
|
del(self.proposed[key])
|
|
del(self.sent[key])
|
|
continue
|
|
|
|
if isinstance(updates, dict):
|
|
# Clean up self.sent
|
|
for key in updates:
|
|
# Always retain 'id'
|
|
if key in required:
|
|
if key in self.existing or updates.get(key) is not None:
|
|
self.sent[key] = updates.get(key)
|
|
continue
|
|
|
|
# Remove unspecified values
|
|
elif not collate and updates.get(key) is None:
|
|
if key in self.existing:
|
|
del(self.sent[key])
|
|
continue
|
|
|
|
# Remove identical values
|
|
elif not collate and updates.get(key) == self.existing.get(key):
|
|
del(self.sent[key])
|
|
continue
|
|
|
|
# Add everything else
|
|
if updates.get(key) is not None:
|
|
self.sent[key] = updates.get(key)
|
|
|
|
# Update self.proposed
|
|
self.proposed.update(self.sent)
|
|
|
|
elif updates is not None:
|
|
self.sent = updates
|
|
# Update self.proposed
|
|
self.proposed = self.sent
|
|
|
|
def exit_json(self, **kwargs):
|
|
''' Custom written method to exit from module. '''
|
|
|
|
if self.params.get('state') in ('absent', 'present', 'upload', 'restore', 'download', 'move', 'clone'):
|
|
if self.params.get('output_level') in ('debug', 'info'):
|
|
self.result['previous'] = self.previous
|
|
# FIXME: Modified header only works for PATCH
|
|
if not self.has_modified and self.previous != self.existing:
|
|
self.result['changed'] = True
|
|
if self.stdout:
|
|
self.result['stdout'] = self.stdout
|
|
|
|
# Return the gory details when we need it
|
|
if self.params.get('output_level') == 'debug':
|
|
self.result['method'] = self.method
|
|
self.result['response'] = self.response
|
|
self.result['status'] = self.status
|
|
self.result['url'] = self.url
|
|
self.result['socket'] = self.module._socket_path
|
|
|
|
if self.params.get('state') in ('absent', 'present'):
|
|
self.result['sent'] = self.sent
|
|
self.result['proposed'] = self.proposed
|
|
|
|
self.result['current'] = self.existing
|
|
|
|
if self.module._diff and self.result.get('changed') is True:
|
|
self.result['diff'] = dict(
|
|
before=self.previous,
|
|
after=self.existing,
|
|
)
|
|
|
|
self.result.update(**kwargs)
|
|
self.module.exit_json(**self.result)
|
|
|
|
def fail_json(self, msg, **kwargs):
|
|
''' Custom written method to return info on failure. '''
|
|
|
|
if self.params.get('state') in ('absent', 'present'):
|
|
if self.params.get('output_level') in ('debug', 'info'):
|
|
self.result['previous'] = self.previous
|
|
# FIXME: Modified header only works for PATCH
|
|
if not self.has_modified and self.previous != self.existing:
|
|
self.result['changed'] = True
|
|
if self.stdout:
|
|
self.result['stdout'] = self.stdout
|
|
|
|
# Return the gory details when we need it
|
|
if self.params.get('output_level') == 'debug':
|
|
if self.url is not None:
|
|
self.result['method'] = self.method
|
|
self.result['response'] = self.response
|
|
self.result['status'] = self.status
|
|
self.result['url'] = self.url
|
|
self.result['socket'] = self.module._socket_path
|
|
|
|
if self.params.get('state') in ('absent', 'present'):
|
|
self.result['sent'] = self.sent
|
|
self.result['proposed'] = self.proposed
|
|
|
|
self.result['current'] = self.existing
|
|
|
|
self.result.update(**kwargs)
|
|
self.module.fail_json(msg=msg, **self.result)
|
|
|
|
def check_changed(self):
|
|
''' Check if changed by comparing new values from existing'''
|
|
existing = self.existing
|
|
if 'password' in existing:
|
|
existing['password'] = self.sent.get('password')
|
|
return not issubset(self.sent, existing)
|
|
|
|
def update_filter_obj(self, contract_obj, filter_obj, filter_type, contract_display_name=None, updateFilterRef=True):
|
|
''' update filter with more information '''
|
|
if updateFilterRef:
|
|
filter_obj['filterRef'] = self.dict_from_ref(filter_obj.get('filterRef'))
|
|
if contract_display_name:
|
|
filter_obj['displayName'] = contract_display_name
|
|
else:
|
|
filter_obj['displayName'] = contract_obj.get('displayName')
|
|
filter_obj['filterType'] = filter_type
|
|
filter_obj['contractScope'] = contract_obj.get('scope')
|
|
filter_obj['contractFilterType'] = contract_obj.get('filterType')
|
|
return filter_obj
|
|
|
|
def query_schema(self, schema):
|
|
schema_id = self.lookup_schema(schema)
|
|
schema_path = "schemas/{0}".format(schema_id)
|
|
schema_obj = self.query_obj(schema_path, displayName=schema)
|
|
if not schema_obj:
|
|
self.module.fail_json(msg="Schema '{0}' is not a valid schema name.".format(schema))
|
|
return schema_id, schema_path, schema_obj
|