diff options
author | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:33 +0000 |
---|---|---|
committer | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:33 +0000 |
commit | 741ff30da275a421dde72d80b53e02cca56ed7e3 (patch) | |
tree | ec179fa62278b1a160f633cc87d6c1d18dac3016 /lib/ansible/module_utils | |
parent | f1c4ac5056704bde3ca936d492d2a731ab38db52 (diff) | |
download | ansible-741ff30da275a421dde72d80b53e02cca56ed7e3.tar.gz |
Migrated to netapp.ontap
Diffstat (limited to 'lib/ansible/module_utils')
-rw-r--r-- | lib/ansible/module_utils/netapp.py | 744 | ||||
-rw-r--r-- | lib/ansible/module_utils/netapp_elementsw_module.py | 156 | ||||
-rw-r--r-- | lib/ansible/module_utils/netapp_module.py | 270 |
3 files changed, 0 insertions, 1170 deletions
diff --git a/lib/ansible/module_utils/netapp.py b/lib/ansible/module_utils/netapp.py deleted file mode 100644 index 5c4d0abd9f..0000000000 --- a/lib/ansible/module_utils/netapp.py +++ /dev/null @@ -1,744 +0,0 @@ -# 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, Sumit Kumar <sumit4@netapp.com> -# Copyright (c) 2017, Michael Price <michael.price@netapp.com> -# 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. - -import json -import os -import random -import mimetypes - -from pprint import pformat -from ansible.module_utils import six -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError -from ansible.module_utils.urls import open_url -from ansible.module_utils.api import basic_auth_argument_spec -from ansible.module_utils._text import to_native - -try: - from ansible.module_utils.ansible_release import __version__ as ansible_version -except ImportError: - ansible_version = 'unknown' - -try: - from netapp_lib.api.zapi import zapi - HAS_NETAPP_LIB = True -except ImportError: - HAS_NETAPP_LIB = False - -try: - import requests - HAS_REQUESTS = True -except ImportError: - HAS_REQUESTS = False - -import ssl -try: - from urlparse import urlparse, urlunparse -except ImportError: - from urllib.parse import urlparse, urlunparse - - -HAS_SF_SDK = False -SF_BYTE_MAP = dict( - # Management GUI displays 1024 ** 3 as 1.1 GB, thus use 1000. - bytes=1, - b=1, - kb=1000, - mb=1000 ** 2, - gb=1000 ** 3, - tb=1000 ** 4, - pb=1000 ** 5, - eb=1000 ** 6, - zb=1000 ** 7, - yb=1000 ** 8 -) - -POW2_BYTE_MAP = dict( - # Here, 1 kb = 1024 - bytes=1, - b=1, - kb=1024, - mb=1024 ** 2, - gb=1024 ** 3, - tb=1024 ** 4, - pb=1024 ** 5, - eb=1024 ** 6, - zb=1024 ** 7, - yb=1024 ** 8 -) - -try: - from solidfire.factory import ElementFactory - from solidfire.custom.models import TimeIntervalFrequency - from solidfire.models import Schedule, ScheduleInfo - - HAS_SF_SDK = True -except Exception: - HAS_SF_SDK = False - - -def has_netapp_lib(): - return HAS_NETAPP_LIB - - -def has_sf_sdk(): - return HAS_SF_SDK - - -def na_ontap_host_argument_spec(): - - return dict( - hostname=dict(required=True, type='str'), - username=dict(required=True, type='str', aliases=['user']), - password=dict(required=True, type='str', aliases=['pass'], no_log=True), - https=dict(required=False, type='bool', default=False), - validate_certs=dict(required=False, type='bool', default=True), - http_port=dict(required=False, type='int'), - ontapi=dict(required=False, type='int'), - use_rest=dict(required=False, type='str', default='Auto', choices=['Never', 'Always', 'Auto']) - ) - - -def ontap_sf_host_argument_spec(): - - return dict( - hostname=dict(required=True, type='str'), - username=dict(required=True, type='str', aliases=['user']), - password=dict(required=True, type='str', aliases=['pass'], no_log=True) - ) - - -def aws_cvs_host_argument_spec(): - - return dict( - api_url=dict(required=True, type='str'), - validate_certs=dict(required=False, type='bool', default=True), - api_key=dict(required=True, type='str'), - secret_key=dict(required=True, type='str') - ) - - -def create_sf_connection(module, port=None): - hostname = module.params['hostname'] - username = module.params['username'] - password = module.params['password'] - - if HAS_SF_SDK and hostname and username and password: - try: - return_val = ElementFactory.create(hostname, username, password, port=port) - return return_val - except Exception: - raise Exception("Unable to create SF connection") - else: - module.fail_json(msg="the python SolidFire SDK module is required") - - -def setup_na_ontap_zapi(module, vserver=None): - hostname = module.params['hostname'] - username = module.params['username'] - password = module.params['password'] - https = module.params['https'] - validate_certs = module.params['validate_certs'] - port = module.params['http_port'] - version = module.params['ontapi'] - - if HAS_NETAPP_LIB: - # set up zapi - server = zapi.NaServer(hostname) - server.set_username(username) - server.set_password(password) - if vserver: - server.set_vserver(vserver) - if version: - minor = version - else: - minor = 110 - server.set_api_version(major=1, minor=minor) - # default is HTTP - if https: - if port is None: - port = 443 - transport_type = 'HTTPS' - # HACK to bypass certificate verification - if validate_certs is False: - if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None): - ssl._create_default_https_context = ssl._create_unverified_context - else: - if port is None: - port = 80 - transport_type = 'HTTP' - server.set_transport_type(transport_type) - server.set_port(port) - server.set_server_type('FILER') - return server - else: - module.fail_json(msg="the python NetApp-Lib module is required") - - -def setup_ontap_zapi(module, vserver=None): - hostname = module.params['hostname'] - username = module.params['username'] - password = module.params['password'] - - if HAS_NETAPP_LIB: - # set up zapi - server = zapi.NaServer(hostname) - server.set_username(username) - server.set_password(password) - if vserver: - server.set_vserver(vserver) - # Todo : Replace hard-coded values with configurable parameters. - server.set_api_version(major=1, minor=110) - server.set_port(80) - server.set_server_type('FILER') - server.set_transport_type('HTTP') - return server - else: - module.fail_json(msg="the python NetApp-Lib module is required") - - -def eseries_host_argument_spec(): - """Retrieve a base argument specification common to all NetApp E-Series modules""" - argument_spec = basic_auth_argument_spec() - argument_spec.update(dict( - api_username=dict(type='str', required=True), - api_password=dict(type='str', required=True, no_log=True), - api_url=dict(type='str', required=True), - ssid=dict(type='str', required=False, default='1'), - validate_certs=dict(type='bool', required=False, default=True) - )) - return argument_spec - - -class NetAppESeriesModule(object): - """Base class for all NetApp E-Series modules. - - Provides a set of common methods for NetApp E-Series modules, including version checking, mode (proxy, embedded) - verification, http requests, secure http redirection for embedded web services, and logging setup. - - Be sure to add the following lines in the module's documentation section: - extends_documentation_fragment: - - netapp.eseries - - :param dict(dict) ansible_options: dictionary of ansible option definitions - :param str web_services_version: minimally required web services rest api version (default value: "02.00.0000.0000") - :param bool supports_check_mode: whether the module will support the check_mode capabilities (default=False) - :param list(list) mutually_exclusive: list containing list(s) of mutually exclusive options (optional) - :param list(list) required_if: list containing list(s) containing the option, the option value, and then - a list of required options. (optional) - :param list(list) required_one_of: list containing list(s) of options for which at least one is required. (optional) - :param list(list) required_together: list containing list(s) of options that are required together. (optional) - :param bool log_requests: controls whether to log each request (default: True) - """ - DEFAULT_TIMEOUT = 60 - DEFAULT_SECURE_PORT = "8443" - DEFAULT_REST_API_PATH = "devmgr/v2/" - DEFAULT_REST_API_ABOUT_PATH = "devmgr/utils/about" - DEFAULT_HEADERS = {"Content-Type": "application/json", "Accept": "application/json", - "netapp-client-type": "Ansible-%s" % ansible_version} - HTTP_AGENT = "Ansible / %s" % ansible_version - SIZE_UNIT_MAP = dict(bytes=1, b=1, kb=1024, mb=1024**2, gb=1024**3, tb=1024**4, - pb=1024**5, eb=1024**6, zb=1024**7, yb=1024**8) - - def __init__(self, ansible_options, web_services_version=None, supports_check_mode=False, - mutually_exclusive=None, required_if=None, required_one_of=None, required_together=None, - log_requests=True): - argument_spec = eseries_host_argument_spec() - argument_spec.update(ansible_options) - - self.module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=supports_check_mode, - mutually_exclusive=mutually_exclusive, required_if=required_if, - required_one_of=required_one_of, required_together=required_together) - - args = self.module.params - self.web_services_version = web_services_version if web_services_version else "02.00.0000.0000" - self.ssid = args["ssid"] - self.url = args["api_url"] - self.log_requests = log_requests - self.creds = dict(url_username=args["api_username"], - url_password=args["api_password"], - validate_certs=args["validate_certs"]) - - if not self.url.endswith("/"): - self.url += "/" - - self.is_embedded_mode = None - self.is_web_services_valid_cache = None - - def _check_web_services_version(self): - """Verify proxy or embedded web services meets minimum version required for module. - - The minimum required web services version is evaluated against version supplied through the web services rest - api. AnsibleFailJson exception will be raised when the minimum is not met or exceeded. - - This helper function will update the supplied api url if secure http is not used for embedded web services - - :raise AnsibleFailJson: raised when the contacted api service does not meet the minimum required version. - """ - if not self.is_web_services_valid_cache: - - url_parts = urlparse(self.url) - if not url_parts.scheme or not url_parts.netloc: - self.module.fail_json(msg="Failed to provide valid API URL. Example: https://192.168.1.100:8443/devmgr/v2. URL [%s]." % self.url) - - if url_parts.scheme not in ["http", "https"]: - self.module.fail_json(msg="Protocol must be http or https. URL [%s]." % self.url) - - self.url = "%s://%s/" % (url_parts.scheme, url_parts.netloc) - about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH - rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, ignore_errors=True, **self.creds) - - if rc != 200: - self.module.warn("Failed to retrieve web services about information! Retrying with secure ports. Array Id [%s]." % self.ssid) - self.url = "https://%s:8443/" % url_parts.netloc.split(":")[0] - about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH - try: - rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds) - except Exception as error: - self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]." - % (self.ssid, to_native(error))) - - major, minor, other, revision = data["version"].split(".") - minimum_major, minimum_minor, other, minimum_revision = self.web_services_version.split(".") - - if not (major > minimum_major or - (major == minimum_major and minor > minimum_minor) or - (major == minimum_major and minor == minimum_minor and revision >= minimum_revision)): - self.module.fail_json(msg="Web services version does not meet minimum version required. Current version: [%s]." - " Version required: [%s]." % (data["version"], self.web_services_version)) - - self.module.log("Web services rest api version met the minimum required version.") - self.is_web_services_valid_cache = True - - def is_embedded(self): - """Determine whether web services server is the embedded web services. - - If web services about endpoint fails based on an URLError then the request will be attempted again using - secure http. - - :raise AnsibleFailJson: raised when web services about endpoint failed to be contacted. - :return bool: whether contacted web services is running from storage array (embedded) or from a proxy. - """ - self._check_web_services_version() - - if self.is_embedded_mode is None: - about_url = self.url + self.DEFAULT_REST_API_ABOUT_PATH - try: - rc, data = request(about_url, timeout=self.DEFAULT_TIMEOUT, headers=self.DEFAULT_HEADERS, **self.creds) - self.is_embedded_mode = not data["runningAsProxy"] - except Exception as error: - self.module.fail_json(msg="Failed to retrieve the webservices about information! Array Id [%s]. Error [%s]." - % (self.ssid, to_native(error))) - - return self.is_embedded_mode - - def request(self, path, data=None, method='GET', headers=None, ignore_errors=False): - """Issue an HTTP request to a url, retrieving an optional JSON response. - - :param str path: web services rest api endpoint path (Example: storage-systems/1/graph). Note that when the - full url path is specified then that will be used without supplying the protocol, hostname, port and rest path. - :param data: data required for the request (data may be json or any python structured data) - :param str method: request method such as GET, POST, DELETE. - :param dict headers: dictionary containing request headers. - :param bool ignore_errors: forces the request to ignore any raised exceptions. - """ - self._check_web_services_version() - - if headers is None: - headers = self.DEFAULT_HEADERS - - if not isinstance(data, str) and headers["Content-Type"] == "application/json": - data = json.dumps(data) - - if path.startswith("/"): - path = path[1:] - request_url = self.url + self.DEFAULT_REST_API_PATH + path - - if self.log_requests or True: - self.module.log(pformat(dict(url=request_url, data=data, method=method))) - - return request(url=request_url, data=data, method=method, headers=headers, use_proxy=True, force=False, last_mod_time=None, - timeout=self.DEFAULT_TIMEOUT, http_agent=self.HTTP_AGENT, force_basic_auth=True, ignore_errors=ignore_errors, **self.creds) - - -def create_multipart_formdata(files, fields=None, send_8kb=False): - """Create the data for a multipart/form request. - - :param list(list) files: list of lists each containing (name, filename, path). - :param list(list) fields: list of lists each containing (key, value). - :param bool send_8kb: only sends the first 8kb of the files (default: False). - """ - boundary = "---------------------------" + "".join([str(random.randint(0, 9)) for x in range(27)]) - data_parts = list() - data = None - - if six.PY2: # Generate payload for Python 2 - newline = "\r\n" - if fields is not None: - for key, value in fields: - data_parts.extend(["--%s" % boundary, - 'Content-Disposition: form-data; name="%s"' % key, - "", - value]) - - for name, filename, path in files: - with open(path, "rb") as fh: - value = fh.read(8192) if send_8kb else fh.read() - - data_parts.extend(["--%s" % boundary, - 'Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename), - "Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream"), - "", - value]) - data_parts.extend(["--%s--" % boundary, ""]) - data = newline.join(data_parts) - - else: - newline = six.b("\r\n") - if fields is not None: - for key, value in fields: - data_parts.extend([six.b("--%s" % boundary), - six.b('Content-Disposition: form-data; name="%s"' % key), - six.b(""), - six.b(value)]) - - for name, filename, path in files: - with open(path, "rb") as fh: - value = fh.read(8192) if send_8kb else fh.read() - - data_parts.extend([six.b("--%s" % boundary), - six.b('Content-Disposition: form-data; name="%s"; filename="%s"' % (name, filename)), - six.b("Content-Type: %s" % (mimetypes.guess_type(path)[0] or "application/octet-stream")), - six.b(""), - value]) - data_parts.extend([six.b("--%s--" % boundary), b""]) - data = newline.join(data_parts) - - headers = { - "Content-Type": "multipart/form-data; boundary=%s" % boundary, - "Content-Length": str(len(data))} - - return headers, data - - -def request(url, data=None, headers=None, method='GET', use_proxy=True, - force=False, last_mod_time=None, timeout=10, validate_certs=True, - url_username=None, url_password=None, http_agent=None, force_basic_auth=True, ignore_errors=False): - """Issue an HTTP request to a url, retrieving an optional JSON response.""" - - if headers is None: - headers = {"Content-Type": "application/json", "Accept": "application/json"} - headers.update({"netapp-client-type": "Ansible-%s" % ansible_version}) - - if not http_agent: - http_agent = "Ansible / %s" % ansible_version - - try: - r = open_url(url=url, data=data, headers=headers, method=method, use_proxy=use_proxy, - force=force, last_mod_time=last_mod_time, timeout=timeout, validate_certs=validate_certs, - url_username=url_username, url_password=url_password, http_agent=http_agent, - force_basic_auth=force_basic_auth) - except HTTPError as err: - r = err.fp - - try: - raw_data = r.read() - if raw_data: - data = json.loads(raw_data) - else: - raw_data = None - except Exception: - if ignore_errors: - pass - else: - raise Exception(raw_data) - - resp_code = r.getcode() - - if resp_code >= 400 and not ignore_errors: - raise Exception(resp_code, data) - else: - return resp_code, data - - -def ems_log_event(source, server, name="Ansible", id="12345", version=ansible_version, - category="Information", event="setup", autosupport="false"): - ems_log = zapi.NaElement('ems-autosupport-log') - # Host name invoking the API. - ems_log.add_new_child("computer-name", name) - # ID of event. A user defined event-id, range [0..2^32-2]. - ems_log.add_new_child("event-id", id) - # Name of the application invoking the API. - ems_log.add_new_child("event-source", source) - # Version of application invoking the API. - ems_log.add_new_child("app-version", version) - # Application defined category of the event. - ems_log.add_new_child("category", category) - # Description of event to log. An application defined message to log. - ems_log.add_new_child("event-description", event) - ems_log.add_new_child("log-level", "6") - ems_log.add_new_child("auto-support", autosupport) - server.invoke_successfully(ems_log, True) - - -def get_cserver_zapi(server): - vserver_info = zapi.NaElement('vserver-get-iter') - query_details = zapi.NaElement.create_node_with_children('vserver-info', **{'vserver-type': 'admin'}) - query = zapi.NaElement('query') - query.add_child_elem(query_details) - vserver_info.add_child_elem(query) - result = server.invoke_successfully(vserver_info, - enable_tunneling=False) - attribute_list = result.get_child_by_name('attributes-list') - vserver_list = attribute_list.get_child_by_name('vserver-info') - return vserver_list.get_child_content('vserver-name') - - -def get_cserver(connection, is_rest=False): - if not is_rest: - return get_cserver_zapi(connection) - - params = {'fields': 'type'} - api = "private/cli/vserver" - json, error = connection.get(api, params) - if json is None or error is not None: - # exit if there is an error or no data - return None - vservers = json.get('records') - if vservers is not None: - for vserver in vservers: - if vserver['type'] == 'admin': # cluster admin - return vserver['vserver'] - if len(vservers) == 1: # assume vserver admin - return vservers[0]['vserver'] - - return None - - -class OntapRestAPI(object): - def __init__(self, module, timeout=60): - self.module = module - self.username = self.module.params['username'] - self.password = self.module.params['password'] - self.hostname = self.module.params['hostname'] - self.use_rest = self.module.params['use_rest'] - self.verify = self.module.params['validate_certs'] - self.timeout = timeout - self.url = 'https://' + self.hostname + '/api/' - self.errors = list() - self.debug_logs = list() - self.check_required_library() - - def check_required_library(self): - if not HAS_REQUESTS: - self.module.fail_json(msg=missing_required_lib('requests')) - - def send_request(self, method, api, params, json=None, return_status_code=False): - ''' send http request and process reponse, including error conditions ''' - url = self.url + api - status_code = None - content = None - json_dict = None - json_error = None - error_details = None - - def get_json(response): - ''' extract json, and error message if present ''' - try: - json = response.json() - except ValueError: - return None, None - error = json.get('error') - return json, error - - try: - response = requests.request(method, url, verify=self.verify, auth=(self.username, self.password), params=params, timeout=self.timeout, json=json) - content = response.content # for debug purposes - status_code = response.status_code - # If the response was successful, no Exception will be raised - response.raise_for_status() - json_dict, json_error = get_json(response) - except requests.exceptions.HTTPError as err: - __, json_error = get_json(response) - if json_error is None: - self.log_error(status_code, 'HTTP error: %s' % err) - error_details = str(err) - # If an error was reported in the json payload, it is handled below - except requests.exceptions.ConnectionError as err: - self.log_error(status_code, 'Connection error: %s' % err) - error_details = str(err) - except Exception as err: - self.log_error(status_code, 'Other error: %s' % err) - error_details = str(err) - if json_error is not None: - self.log_error(status_code, 'Endpoint error: %d: %s' % (status_code, json_error)) - error_details = json_error - self.log_debug(status_code, content) - if return_status_code: - return status_code, error_details - return json_dict, error_details - - def get(self, api, params): - method = 'GET' - return self.send_request(method, api, params) - - def post(self, api, data, params=None): - method = 'POST' - return self.send_request(method, api, params, json=data) - - def patch(self, api, data, params=None): - method = 'PATCH' - return self.send_request(method, api, params, json=data) - - def delete(self, api, data, params=None): - method = 'DELETE' - return self.send_request(method, api, params, json=data) - - def _is_rest(self, used_unsupported_rest_properties=None): - if self.use_rest == "Always": - if used_unsupported_rest_properties: - error = "REST API currently does not support '%s'" % \ - ', '.join(used_unsupported_rest_properties) - return True, error - else: - return True, None - if self.use_rest == 'Never' or used_unsupported_rest_properties: - # force ZAPI if requested or if some parameter requires it - return False, None - method = 'HEAD' - api = 'cluster/software' - status_code, __ = self.send_request(method, api, params=None, return_status_code=True) - if status_code == 200: - return True, None - return False, None - - def is_rest(self, used_unsupported_rest_properties=None): - ''' only return error if there is a reason to ''' - use_rest, error = self._is_rest(used_unsupported_rest_properties) - if used_unsupported_rest_properties is None: - return use_rest - return use_rest, error - - def log_error(self, status_code, message): - self.errors.append(message) - self.debug_logs.append((status_code, message)) - - def log_debug(self, status_code, content): - self.debug_logs.append((status_code, content)) - - -class AwsCvsRestAPI(object): - def __init__(self, module, timeout=60): - self.module = module - self.api_key = self.module.params['api_key'] - self.secret_key = self.module.params['secret_key'] - self.api_url = self.module.params['api_url'] - self.verify = self.module.params['validate_certs'] - self.timeout = timeout - self.url = 'https://' + self.api_url + '/v1/' - self.check_required_library() - - def check_required_library(self): - if not HAS_REQUESTS: - self.module.fail_json(msg=missing_required_lib('requests')) - - def send_request(self, method, api, params, json=None): - ''' send http request and process reponse, including error conditions ''' - url = self.url + api - status_code = None - content = None - json_dict = None - json_error = None - error_details = None - headers = { - 'Content-type': "application/json", - 'api-key': self.api_key, - 'secret-key': self.secret_key, - 'Cache-Control': "no-cache", - } - - def get_json(response): - ''' extract json, and error message if present ''' - try: - json = response.json() - - except ValueError: - return None, None - success_code = [200, 201, 202] - if response.status_code not in success_code: - error = json.get('message') - else: - error = None - return json, error - try: - response = requests.request(method, url, headers=headers, timeout=self.timeout, json=json) - status_code = response.status_code - # If the response was successful, no Exception will be raised - json_dict, json_error = get_json(response) - except requests.exceptions.HTTPError as err: - __, json_error = get_json(response) - if json_error is None: - error_details = str(err) - except requests.exceptions.ConnectionError as err: - error_details = str(err) - except Exception as err: - error_details = str(err) - if json_error is not None: - error_details = json_error - - return json_dict, error_details - - # If an error was reported in the json payload, it is handled below - def get(self, api, params=None): - method = 'GET' - return self.send_request(method, api, params) - - def post(self, api, data, params=None): - method = 'POST' - return self.send_request(method, api, params, json=data) - - def patch(self, api, data, params=None): - method = 'PATCH' - return self.send_request(method, api, params, json=data) - - def put(self, api, data, params=None): - method = 'PUT' - return self.send_request(method, api, params, json=data) - - def delete(self, api, data, params=None): - method = 'DELETE' - return self.send_request(method, api, params, json=data) - - def get_state(self, jobId): - """ Method to get the state of the job """ - method = 'GET' - response, status_code = self.get('Jobs/%s' % jobId) - while str(response['state']) not in 'done': - response, status_code = self.get('Jobs/%s' % jobId) - return 'done' diff --git a/lib/ansible/module_utils/netapp_elementsw_module.py b/lib/ansible/module_utils/netapp_elementsw_module.py deleted file mode 100644 index c9baa0aeb9..0000000000 --- a/lib/ansible/module_utils/netapp_elementsw_module.py +++ /dev/null @@ -1,156 +0,0 @@ -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. - -HAS_SF_SDK = False -try: - import solidfire.common - - HAS_SF_SDK = True -except Exception: - HAS_SF_SDK = False - - -def has_sf_sdk(): - return HAS_SF_SDK - - -class NaElementSWModule(object): - - def __init__(self, elem): - self.elem_connect = elem - self.parameters = dict() - - def get_volume(self, volume_id): - """ - Return volume details if volume exists for given volume_id - - :param volume_id: volume ID - :type volume_id: int - :return: Volume dict if found, None if not found - :rtype: dict - """ - volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id]) - for volume in volume_list.volumes: - if volume.volume_id == volume_id: - if str(volume.delete_time) == "": - return volume - return None - - def get_volume_id(self, vol_name, account_id): - """ - Return volume id from the given (valid) account_id if found - Return None if not found - - :param vol_name: Name of the volume - :type vol_name: str - :param account_id: Account ID - :type account_id: int - - :return: Volume ID of the first matching volume if found. None if not found. - :rtype: int - """ - volume_list = self.elem_connect.list_volumes_for_account(account_id=account_id) - for volume in volume_list.volumes: - if volume.name == vol_name: - # return volume_id - if str(volume.delete_time) == "": - return volume.volume_id - return None - - def volume_id_exists(self, volume_id): - """ - Return volume_id if volume exists for given volume_id - - :param volume_id: volume ID - :type volume_id: int - :return: Volume ID if found, None if not found - :rtype: int - """ - volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id]) - for volume in volume_list.volumes: - if volume.volume_id == volume_id: - if str(volume.delete_time) == "": - return volume.volume_id - return None - - def volume_exists(self, volume, account_id): - """ - Return volume_id if exists, None if not found - - :param volume: Volume ID or Name - :type volume: str - :param account_id: Account ID (valid) - :type account_id: int - :return: Volume ID if found, None if not found - """ - # If volume is an integer, get_by_id - if str(volume).isdigit(): - volume_id = int(volume) - try: - if self.volume_id_exists(volume_id): - return volume_id - except solidfire.common.ApiServerError: - # don't fail, continue and try get_by_name - pass - # get volume by name - volume_id = self.get_volume_id(volume, account_id) - return volume_id - - def get_snapshot(self, snapshot_id, volume_id): - """ - Return snapshot details if found - - :param snapshot_id: Snapshot ID or Name - :type snapshot_id: str - :param volume_id: Account ID (valid) - :type volume_id: int - :return: Snapshot dict if found, None if not found - :rtype: dict - """ - # mandate src_volume_id although not needed by sdk - snapshot_list = self.elem_connect.list_snapshots( - volume_id=volume_id) - for snapshot in snapshot_list.snapshots: - # if actual id is provided - if str(snapshot_id).isdigit() and snapshot.snapshot_id == int(snapshot_id): - return snapshot - # if snapshot name is provided - elif snapshot.name == snapshot_id: - return snapshot - return None - - def account_exists(self, account): - """ - Return account_id if account exists for given account id or name - Raises an exception if account does not exist - - :param account: Account ID or Name - :type account: str - :return: Account ID if found, None if not found - """ - # If account is an integer, get_by_id - if account.isdigit(): - account_id = int(account) - try: - result = self.elem_connect.get_account_by_id(account_id=account_id) - if result.account.account_id == account_id: - return account_id - except solidfire.common.ApiServerError: - # don't fail, continue and try get_by_name - pass - # get account by name, the method returns an Exception if account doesn't exist - result = self.elem_connect.get_account_by_name(username=account) - return result.account.account_id - - def set_element_attributes(self, source): - """ - Return telemetry attributes for the current execution - - :param source: name of the module - :type source: str - :return: a dict containing telemetry attributes - """ - attributes = {} - attributes['config-mgmt'] = 'ansible' - attributes['event-source'] = source - return(attributes) diff --git a/lib/ansible/module_utils/netapp_module.py b/lib/ansible/module_utils/netapp_module.py deleted file mode 100644 index 914236b349..0000000000 --- a/lib/ansible/module_utils/netapp_module.py +++ /dev/null @@ -1,270 +0,0 @@ -# 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) 2018, Laurent Nicolas <laurentn@netapp.com> -# 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. - -''' Support class for NetApp ansible modules ''' - -import ansible.module_utils.netapp as netapp_utils - - -def cmp(a, b): - """ - Python 3 does not have a cmp function, this will do the cmp. - :param a: first object to check - :param b: second object to check - :return: - """ - # convert to lower case for string comparison. - if a is None: - return -1 - if type(a) is str and type(b) is str: - a = a.lower() - b = b.lower() - # if list has string element, convert string to lower case. - if type(a) is list and type(b) is list: - a = [x.lower() if type(x) is str else x for x in a] - b = [x.lower() if type(x) is str else x for x in b] - a.sort() - b.sort() - return (a > b) - (a < b) - - -class NetAppModule(object): - ''' - Common class for NetApp modules - set of support functions to derive actions based - on the current state of the system, and a desired state - ''' - - def __init__(self): - self.log = list() - self.changed = False - self.parameters = {'name': 'not intialized'} - self.zapi_string_keys = dict() - self.zapi_bool_keys = dict() - self.zapi_list_keys = dict() - self.zapi_int_keys = dict() - self.zapi_required = dict() - - def set_parameters(self, ansible_params): - self.parameters = dict() - for param in ansible_params: - if ansible_params[param] is not None: - self.parameters[param] = ansible_params[param] - return self.parameters - - def get_value_for_bool(self, from_zapi, value): - """ - Convert boolean values to string or vice-versa - If from_zapi = True, value is converted from string (as it appears in ZAPI) to boolean - If from_zapi = False, value is converted from boolean to string - For get() method, from_zapi = True - For modify(), create(), from_zapi = False - :param from_zapi: convert the value from ZAPI or to ZAPI acceptable type - :param value: value of the boolean attribute - :return: string or boolean - """ - if value is None: - return None - if from_zapi: - return True if value == 'true' else False - else: - return 'true' if value else 'false' - - def get_value_for_int(self, from_zapi, value): - """ - Convert integer values to string or vice-versa - If from_zapi = True, value is converted from string (as it appears in ZAPI) to integer - If from_zapi = False, value is converted from integer to string - For get() method, from_zapi = True - For modify(), create(), from_zapi = False - :param from_zapi: convert the value from ZAPI or to ZAPI acceptable type - :param value: value of the integer attribute - :return: string or integer - """ - if value is None: - return None - if from_zapi: - return int(value) - else: - return str(value) - - def get_value_for_list(self, from_zapi, zapi_parent, zapi_child=None, data=None): - """ - Convert a python list() to NaElement or vice-versa - If from_zapi = True, value is converted from NaElement (parent-children structure) to list() - If from_zapi = False, value is converted from list() to NaElement - :param zapi_parent: ZAPI parent key or the ZAPI parent NaElement - :param zapi_child: ZAPI child key - :param data: list() to be converted to NaElement parent-children object - :param from_zapi: convert the value from ZAPI or to ZAPI acceptable type - :return: list() or NaElement - """ - if from_zapi: - if zapi_parent is None: - return [] - else: - return [zapi_child.get_content() for zapi_child in zapi_parent.get_children()] - else: - zapi_parent = netapp_utils.zapi.NaElement(zapi_parent) - for item in data: - zapi_parent.add_new_child(zapi_child, item) - return zapi_parent - - def get_cd_action(self, current, desired): - ''' takes a desired state and a current state, and return an action: - create, delete, None - eg: - is_present = 'absent' - some_object = self.get_object(source) - if some_object is not None: - is_present = 'present' - action = cd_action(current=is_present, desired = self.desired.state()) - ''' - if 'state' in desired: - desired_state = desired['state'] - else: - desired_state = 'present' - - if current is None and desired_state == 'absent': - return None - if current is not None and desired_state == 'present': - return None - # change in state - self.changed = True - if current is not None: - return 'delete' - return 'create' - - def compare_and_update_values(self, current, desired, keys_to_compare): - updated_values = dict() - is_changed = False - for key in keys_to_compare: - if key in current: - if key in desired and desired[key] is not None: - if current[key] != desired[key]: - updated_values[key] = desired[key] - is_changed = True - else: - updated_values[key] = current[key] - else: - updated_values[key] = current[key] - - return updated_values, is_changed - - @staticmethod - def check_keys(current, desired): - ''' TODO: raise an error if keys do not match - with the exception of: - new_name, state in desired - ''' - pass - - @staticmethod - def compare_lists(current, desired, get_list_diff): - ''' compares two lists and return a list of elements that are either the desired elements or elements that are - modified from the current state depending on the get_list_diff flag - :param: current: current item attribute in ONTAP - :param: desired: attributes from playbook - :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute - :return: list of attributes to be modified - :rtype: list - ''' - desired_diff_list = [item for item in desired if item not in current] # get what in desired and not in current - current_diff_list = [item for item in current if item not in desired] # get what in current but not in desired - - if desired_diff_list or current_diff_list: - # there are changes - if get_list_diff: - return desired_diff_list - else: - return desired - else: - return [] - - def get_modified_attributes(self, current, desired, get_list_diff=False): - ''' takes two dicts of attributes and return a dict of attributes that are - not in the current state - It is expected that all attributes of interest are listed in current and - desired. - :param: current: current attributes in ONTAP - :param: desired: attributes from playbook - :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute - :return: dict of attributes to be modified - :rtype: dict - - NOTE: depending on the attribute, the caller may need to do a modify or a - different operation (eg move volume if the modified attribute is an - aggregate name) - ''' - # if the object does not exist, we can't modify it - modified = dict() - if current is None: - return modified - - # error out if keys do not match - self.check_keys(current, desired) - - # collect changed attributes - for key, value in current.items(): - if key in desired and desired[key] is not None: - if type(value) is list: - modified_list = self.compare_lists(value, desired[key], get_list_diff) # get modified list from current and desired - if modified_list: - modified[key] = modified_list - elif cmp(value, desired[key]) != 0: - modified[key] = desired[key] - if modified: - self.changed = True - return modified - - def is_rename_action(self, source, target): - ''' takes a source and target object, and returns True - if a rename is required - eg: - source = self.get_object(source_name) - target = self.get_object(target_name) - action = is_rename_action(source, target) - :return: None for error, True for rename action, False otherwise - ''' - if source is None and target is None: - # error, do nothing - # cannot rename an non existent resource - # alternatively we could create B - return None - if source is not None and target is not None: - # error, do nothing - # idempotency (or) new_name_is_already_in_use - # alternatively we could delete B and rename A to B - return False - if source is None and target is not None: - # do nothing, maybe the rename was already done - return False - # source is not None and target is None: - # rename is in order - self.changed = True - return True |