summaryrefslogtreecommitdiffstats
path: root/ansible_collections/netapp/ontap/plugins/module_utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/netapp/ontap/plugins/module_utils
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/netapp/ontap/plugins/module_utils')
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/netapp.py1134
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/netapp_elementsw_module.py41
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/netapp_ipaddress.py134
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/netapp_module.py619
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_application.py180
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_generic.py101
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_owning_resource.py26
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_response_helpers.py137
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_user.py49
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_volume.py61
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/rest_vserver.py61
-rw-r--r--ansible_collections/netapp/ontap/plugins/module_utils/zapis_svm.py133
12 files changed, 2676 insertions, 0 deletions
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py
new file mode 100644
index 000000000..28d9428a2
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp.py
@@ -0,0 +1,1134 @@
+# 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>
+# Copyright (c) 2017-2023, NetApp, Inc
+# 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.
+
+'''
+netapp.py
+'''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import base64
+import logging
+import os
+import ssl
+import time
+from ansible.module_utils.basic import missing_required_lib
+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'
+
+COLLECTION_VERSION = "22.7.0"
+CLIENT_APP_VERSION = "%s/%s" % ("%s", COLLECTION_VERSION)
+IMPORT_EXCEPTION = None
+
+try:
+ from netapp_lib.api.zapi import zapi
+ HAS_NETAPP_LIB = True
+except ImportError as exc:
+ HAS_NETAPP_LIB = False
+ IMPORT_EXCEPTION = exc
+
+try:
+ import requests
+ HAS_REQUESTS = True
+except ImportError:
+ HAS_REQUESTS = False
+
+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,
+ k=1024,
+ m=1024 ** 2,
+ g=1024 ** 3,
+ t=1024 ** 4,
+ p=1024 ** 5,
+ e=1024 ** 6,
+ z=1024 ** 7,
+ y=1024 ** 8,
+ kb=1024,
+ mb=1024 ** 2,
+ gb=1024 ** 3,
+ tb=1024 ** 4,
+ pb=1024 ** 5,
+ eb=1024 ** 6,
+ zb=1024 ** 7,
+ yb=1024 ** 8,
+)
+
+ERROR_MSG = dict(
+ no_cserver='This module is expected to run as cluster admin'
+)
+
+LOG = logging.getLogger(__name__)
+LOG_FILE = '/tmp/ontap_apis.log'
+ZAPI_DEPRECATION_MESSAGE = "With version 22.0.0 ONTAPI (ZAPI) has been deprecated. The final ONTAP version to support ZAPI is ONTAP 9.13.1. "\
+ "ZAPI calls in these modules will continue to work for ONTAP versions that supports ZAPI. "\
+ "You can update your playbook to use REST by adding use_rest: always to your playbook. "\
+ "More information can be found at: https://github.com/ansible-collections/netapp.ontap"
+
+try:
+ from solidfire.factory import ElementFactory
+ HAS_SF_SDK = True
+except ImportError:
+ HAS_SF_SDK = False
+
+
+def has_netapp_lib():
+ return HAS_NETAPP_LIB
+
+
+def netapp_lib_is_required():
+ return "Error: the python NetApp-Lib module is required. Import error: %s" % str(IMPORT_EXCEPTION)
+
+
+def has_sf_sdk():
+ return HAS_SF_SDK
+
+
+def na_ontap_zapi_only_spec():
+ # This is used for Zapi only Modules.
+
+ return dict(
+ hostname=dict(required=True, type='str'),
+ username=dict(required=False, type='str', aliases=['user']),
+ password=dict(required=False, 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='never'),
+ feature_flags=dict(required=False, type='dict'),
+ cert_filepath=dict(required=False, type='str'),
+ key_filepath=dict(required=False, type='str', no_log=False),
+ )
+
+
+def na_ontap_host_argument_spec():
+ # This is used for Zapi + REST, and REST only Modules.
+
+ return dict(
+ hostname=dict(required=True, type='str'),
+ username=dict(required=False, type='str', aliases=['user']),
+ password=dict(required=False, 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'),
+ feature_flags=dict(required=False, type='dict'),
+ cert_filepath=dict(required=False, type='str'),
+ key_filepath=dict(required=False, type='str', no_log=False),
+ force_ontap_version=dict(required=False, type='str')
+ )
+
+
+def na_ontap_host_argument_spec_peer():
+ spec = na_ontap_host_argument_spec()
+ spec.pop('feature_flags')
+ # get rid of default values, as we'll use source values
+ for value in spec.values():
+ if 'default' in value:
+ value.pop('default')
+ return spec
+
+
+def has_feature(module, feature_name):
+ feature = get_feature(module, feature_name)
+ if isinstance(feature, bool):
+ return feature
+ module.fail_json(msg="Error: expected bool type for feature flag: %s" % feature_name)
+
+
+def get_feature(module, feature_name):
+ ''' if the user has configured the feature, use it
+ otherwise, use our default
+ '''
+ default_flags = dict(
+ strict_json_check=True, # when true, fail if response.content in not empty and is not valid json
+ trace_apis=False, # when true, append ZAPI and REST requests/responses to /tmp/ontap_zapi.txt
+ trace_headers=False, # when true, headers are not redacted in send requests
+ trace_auth_args=False, # when true, auth_args are not redacted in send requests
+ check_required_params_for_none=True,
+ classic_basic_authorization=False, # use ZAPI wrapper to send Authorization header
+ deprecation_warning=True,
+ sanitize_xml=True,
+ sanitize_code_points=[8], # unicode values, 8 is backspace
+ show_modified=True,
+ always_wrap_zapi=True, # for better error reporting
+ flexcache_delete_return_timeout=5, # ONTAP bug if too big?
+ # for SVM, whch protocols can be allowed
+ svm_allowable_protocols_rest=['cifs', 'fcp', 'iscsi', 'nvme', 'nfs', 'ndmp'],
+ svm_allowable_protocols_zapi=['cifs', 'fcp', 'iscsi', 'nvme', 'nfs', 'ndmp', 'http'],
+ max_files_change_threshold=1, # percentage of increase/decrease required to trigger a modify action
+ warn_or_fail_on_fabricpool_backend_change='fail',
+ no_cserver_ems=False # when True, don't attempt to find cserver and don't send cserver EMS
+ )
+
+ if module.params['feature_flags'] is not None and feature_name in module.params['feature_flags']:
+ return module.params['feature_flags'][feature_name]
+ if feature_name in default_flags:
+ return default_flags[feature_name]
+ module.fail_json(msg="Internal error: unexpected feature flag: %s" % feature_name)
+
+
+def create_sf_connection(module, port=None, host_options=None):
+ if not HAS_SF_SDK:
+ module.fail_json(msg="the python SolidFire SDK module is required")
+
+ if host_options is None:
+ host_options = module.params
+ msg, msg2 = None, None
+ missing_options = [option for option in ('hostname', 'username', 'password') if not host_options.get(option)]
+ if missing_options:
+ verb = 'are' if len(missing_options) > 1 else 'is'
+ msg = "%s %s required for ElementSW connection." % (', '.join(missing_options), verb)
+ extra_options = [option for option in ('cert_filepath', 'key_filepath') if host_options.get(option)]
+ if extra_options:
+ verb = 'are' if len(extra_options) > 1 else 'is'
+ msg2 = "%s %s not supported for ElementSW connection." % (', '.join(extra_options), verb)
+ msg = "%s %s" % (msg, msg2) if msg and msg2 else msg or msg2
+ if msg:
+ module.fail_json(msg=msg)
+ hostname = host_options.get('hostname')
+ username = host_options.get('username')
+ password = host_options.get('password')
+
+ try:
+ return ElementFactory.create(hostname, username, password, port=port)
+ except Exception as exc:
+ raise Exception("Unable to create SF connection: %s" % exc)
+
+
+def set_auth_method(module, username, password, cert_filepath, key_filepath):
+ error = None
+ if password is None and username is None:
+ if cert_filepath is None:
+ error = ('Error: cannot have a key file without a cert file' if key_filepath is not None
+ else 'Error: ONTAP module requires username/password or SSL certificate file(s)')
+ else:
+ auth_method = 'single_cert' if key_filepath is None else 'cert_key'
+ elif password is not None and username is not None:
+ if cert_filepath is not None or key_filepath is not None:
+ error = 'Error: cannot have both basic authentication (username/password) ' +\
+ 'and certificate authentication (cert/key files)'
+ else:
+ auth_method = 'basic_auth' if has_feature(module, 'classic_basic_authorization') else 'speedy_basic_auth'
+ else:
+ error = 'Error: username and password have to be provided together'
+ if cert_filepath is not None or key_filepath is not None:
+ error += ' and cannot be used with cert or key files'
+ if error:
+ module.fail_json(msg=error)
+ return auth_method
+
+
+def setup_host_options_from_module_params(host_options, module, keys):
+ '''if an option is not set, use primary value.
+ but don't mix up basic and certificate authentication methods
+
+ host_options is updated in place
+ option values are read from module.params
+ keys is a list of keys that need to be added/updated/left alone in host_options
+ '''
+ password_keys = ['username', 'password']
+ certificate_keys = ['cert_filepath', 'key_filepath']
+ use_password = any(host_options.get(x) is not None for x in password_keys)
+ use_certificate = any(host_options.get(x) is not None for x in certificate_keys)
+ if use_password and use_certificate:
+ module.fail_json(
+ msg='Error: host cannot have both basic authentication (username/password) and certificate authentication (cert/key files).')
+ if use_password:
+ exclude_keys = certificate_keys
+ elif use_certificate:
+ exclude_keys = password_keys
+ else:
+ exclude_keys = []
+ for key in keys:
+ if host_options.get(key) is None and key not in exclude_keys:
+ # use same value as source if no value is given for dest
+ host_options[key] = module.params[key]
+
+
+def set_zapi_port_and_transport(server, https, port, validate_certs):
+ # default is HTTP
+ if https:
+ if port is None:
+ port = 443
+ transport_type = 'HTTPS'
+ # HACK to bypass certificate verification
+ if validate_certs is False and 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)
+
+
+def setup_na_ontap_zapi(module, vserver=None, wrap_zapi=False, host_options=None):
+ module.warn(ZAPI_DEPRECATION_MESSAGE)
+ if host_options is None:
+ host_options = module.params
+ hostname = host_options.get('hostname')
+ username = host_options.get('username')
+ password = host_options.get('password')
+ cert_filepath = host_options.get('cert_filepath')
+ key_filepath = host_options.get('key_filepath')
+ https = host_options.get('https')
+ validate_certs = host_options.get('validate_certs')
+ port = host_options.get('http_port')
+ version = host_options.get('ontapi')
+ trace = has_feature(module, 'trace_apis')
+ if trace:
+ logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s')
+ wrap_zapi |= has_feature(module, 'always_wrap_zapi')
+ auth_method = set_auth_method(module, username, password, cert_filepath, key_filepath)
+
+ if not HAS_NETAPP_LIB:
+ module.fail_json(msg=netapp_lib_is_required())
+
+ # set up zapi
+ if auth_method in ('single_cert', 'cert_key'):
+ # override NaServer in netapp-lib to enable certificate authentication
+ server = OntapZAPICx(hostname, module=module, username=username, password=password,
+ validate_certs=validate_certs, cert_filepath=cert_filepath,
+ key_filepath=key_filepath, style=zapi.NaServer.STYLE_CERTIFICATE,
+ auth_method=auth_method, trace=trace)
+ # SSL certificate authentication requires SSL
+ https = True
+ elif auth_method == 'speedy_basic_auth' or wrap_zapi:
+ # override NaServer in netapp-lib to add Authorization header preemptively
+ # use wrapper to handle parse error (mostly for na_ontap_command)
+ server = OntapZAPICx(hostname, module=module, username=username, password=password,
+ validate_certs=validate_certs, auth_method=auth_method, trace=trace)
+ else:
+ # legacy netapp-lib
+ server = zapi.NaServer(hostname, username=username, password=password, trace=trace)
+ if vserver:
+ server.set_vserver(vserver)
+ if host_options.get('use_rest') == 'always':
+ note = '' if https else ' Note: https is set to false.'
+ module.warn("Using ZAPI for %s, ignoring 'use_rest: always'.%s" % (module._name, note))
+
+ set_zapi_port_and_transport(server, https, port, validate_certs)
+ server.set_api_version(major=1, minor=(version or 110))
+ server.set_server_type('FILER')
+ return server
+
+
+def is_zapi_connection_error(message):
+ ''' return True if it is a connection issue '''
+ # netapp-lib message may contain a tuple or a str!
+ try:
+ if isinstance(message, tuple) and isinstance(message[0], ConnectionError):
+ return True
+ except NameError:
+ # python 2.7 does not know about ConnectionError
+ pass
+ return isinstance(message, str) and message.startswith(('URLError', 'Unauthorized'))
+
+
+def is_zapi_write_access_error(message):
+ ''' return True if it is a write access error '''
+ # netapp-lib message may contain a tuple or a str!
+ if isinstance(message, str) and message.startswith('Insufficient privileges:'):
+ return 'does not have write access' in message
+ return False
+
+
+def is_zapi_missing_vserver_error(message):
+ ''' return True if it is a missing vserver error '''
+ # netapp-lib message may contain a tuple or a str!
+ return isinstance(message, str) and message in ('Vserver API missing vserver parameter.', 'Specified vserver not found')
+
+
+def get_cserver_zapi(server):
+ ''' returns None if not run on the management or cluster IP '''
+ 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)
+ try:
+ result = server.invoke_successfully(vserver_info,
+ enable_tunneling=False)
+ except zapi.NaApiError as exc:
+ # Do not fail if we can't connect to the server.
+ # The module will report a better error when trying to get some data from ONTAP.
+ if is_zapi_connection_error(exc.message):
+ return None
+ # raise on other errors, as it may be a bug in calling the ZAPI
+ raise exc
+ attribute_list = result.get_child_by_name('attributes-list')
+ if attribute_list is not None:
+ vserver_list = attribute_list.get_child_by_name('vserver-info')
+ if vserver_list is not None:
+ return vserver_list.get_child_content('vserver-name')
+ return None
+
+
+def classify_zapi_exception(error):
+ ''' return type of error '''
+ try:
+ # very unlikely to fail, but don't take any chance
+ err_code = int(error.code)
+ except (AttributeError, ValueError):
+ err_code = 0
+ try:
+ # very unlikely to fail, but don't take any chance
+ err_msg = error.message
+ except AttributeError:
+ err_msg = ""
+ if err_code == 13005 and err_msg.startswith('Unable to find API:') and 'data vserver' in err_msg:
+ return 'missing_vserver_api_error', 'Most likely running a cluster level API as vserver: %s' % to_native(error)
+ if err_code == 13001 and err_msg.startswith("RPC: Couldn't make connection"):
+ return 'rpc_error', to_native(error)
+ return "other_error", to_native(error)
+
+
+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
+
+
+def generate_result(changed, actions=None, modify=None, response=None, extra_responses=None):
+ result = dict(changed=changed)
+ if response is not None:
+ result['response'] = response
+ if modify:
+ result['modify'] = modify
+ if actions:
+ result['actions'] = actions
+ if extra_responses:
+ result.update(extra_responses)
+ return result
+
+
+if HAS_NETAPP_LIB:
+ class OntapZAPICx(zapi.NaServer):
+ ''' override zapi NaServer class to:
+ - enable SSL certificate authentication
+ - ignore invalid XML characters in ONTAP output (when using CLI module)
+ - add Authorization header when using basic authentication
+ '''
+ def __init__(self, hostname=None, server_type=zapi.NaServer.SERVER_TYPE_FILER,
+ transport_type=zapi.NaServer.TRANSPORT_TYPE_HTTP,
+ style=zapi.NaServer.STYLE_LOGIN_PASSWORD, username=None,
+ password=None, port=None, trace=False, module=None,
+ cert_filepath=None, key_filepath=None, validate_certs=None,
+ auth_method=None):
+ # python 2.x syntax, but works for python 3 as well
+ super(OntapZAPICx, self).__init__(hostname, server_type=server_type,
+ transport_type=transport_type,
+ style=style, username=username,
+ password=password, port=port, trace=trace)
+ self.cert_filepath = cert_filepath
+ self.key_filepath = key_filepath
+ self.validate_certs = validate_certs
+ self.module = module
+ self.base64_creds = None
+ if auth_method == 'speedy_basic_auth':
+ auth = '%s:%s' % (username, password)
+ self.base64_creds = base64.b64encode(auth.encode()).decode()
+
+ def _create_certificate_auth_handler(self):
+ try:
+ context = ssl.create_default_context()
+ except AttributeError as exc:
+ self._fail_with_exc_info('SSL certificate authentication requires python 2.7 or later.', exc)
+
+ if not self.validate_certs:
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ try:
+ context.load_cert_chain(self.cert_filepath, keyfile=self.key_filepath)
+ except IOError as exc:
+ self._fail_with_exc_info('Cannot load SSL certificate, check files exist.', exc)
+
+ return zapi.urllib.request.HTTPSHandler(context=context)
+
+ def _fail_with_exc_info(self, arg0, exc):
+ msg = arg0
+ msg += ' More info: %s' % repr(exc)
+ self.module.fail_json(msg=msg)
+
+ def sanitize_xml(self, response):
+ # some ONTAP CLI commands return BEL on error
+ new_response = response.replace(b'\x07\n', b'')
+ # And 9.1 uses \r\n rather than \n !
+ new_response = new_response.replace(b'\x07\r\n', b'')
+ # And 9.7 may send backspaces
+ for code_point in get_feature(self.module, 'sanitize_code_points'):
+ if bytes([8]) == b'\x08': # python 3
+ byte = bytes([code_point])
+ elif chr(8) == b'\x08': # python 2
+ byte = chr(code_point)
+ else: # very unlikely, noop
+ byte = b'.'
+ new_response = new_response.replace(byte, b'.')
+ return new_response
+
+ def _parse_response(self, response):
+ ''' handling XML parsing exception '''
+ try:
+ return super(OntapZAPICx, self)._parse_response(response)
+ except zapi.etree.XMLSyntaxError as exc:
+ if has_feature(self.module, 'sanitize_xml'):
+ try:
+ return super(OntapZAPICx, self)._parse_response(self.sanitize_xml(response))
+ except Exception:
+ # ignore a second exception, we'll report the first one
+ pass
+ try:
+ # report first exception, but include full response
+ exc.msg += ". Received: %s" % response
+ except Exception:
+ # in case the response is very badly formatted, ignore it
+ pass
+ raise exc
+
+ def _create_request(self, na_element, enable_tunneling=False):
+ ''' intercept newly created request to add Authorization header '''
+ request, netapp_element = super(OntapZAPICx, self)._create_request(na_element, enable_tunneling=enable_tunneling)
+ request.add_header('X-Dot-Client-App', CLIENT_APP_VERSION % self.module._name)
+ if self.base64_creds is not None:
+ request.add_header('Authorization', 'Basic %s' % self.base64_creds)
+ return request, netapp_element
+
+ # as is from latest version of netapp-lib
+ def invoke_elem(self, na_element, enable_tunneling=False):
+ """Invoke the API on the server."""
+ if not na_element or not isinstance(na_element, zapi.NaElement):
+ raise ValueError('NaElement must be supplied to invoke API')
+
+ request, request_element = self._create_request(na_element,
+ enable_tunneling)
+
+ if self._trace:
+ zapi.LOG.debug("Request: %s", request_element.to_string(pretty=True))
+
+ if not hasattr(self, '_opener') or not self._opener \
+ or self._refresh_conn:
+ self._build_opener()
+ try:
+ if hasattr(self, '_timeout'):
+ response = self._opener.open(request, timeout=self._timeout)
+ else:
+ response = self._opener.open(request)
+ except zapi.urllib.error.HTTPError as exc:
+ raise zapi.NaApiError(exc.code, exc.reason)
+ except zapi.urllib.error.URLError as exc:
+ msg = 'URL error'
+ error = repr(exc)
+ try:
+ # ConnectionRefusedError is not defined in python 2.7
+ if isinstance(exc.reason, ConnectionRefusedError):
+ msg = 'Unable to connect'
+ error = exc.args
+ except Exception:
+ pass
+ raise zapi.NaApiError(msg, error)
+ except Exception as exc:
+ raise zapi.NaApiError('Unexpected error', repr(exc))
+
+ response_xml = response.read()
+ response_element = self._get_result(response_xml)
+
+ if self._trace:
+ zapi.LOG.debug("Response: %s", response_element.to_string(pretty=True))
+
+ return response_element
+
+
+class OntapRestAPI(object):
+ ''' wrapper to send requests to ONTAP REST APIs '''
+ def __init__(self, module, timeout=60, host_options=None):
+ self.host_options = module.params if host_options is None else host_options
+ self.module = module
+ # either username/password or a certifcate with/without a key are used for authentication
+ self.username = self.host_options.get('username')
+ self.password = self.host_options.get('password')
+ self.hostname = self.host_options['hostname']
+ self.use_rest = self.host_options['use_rest'].lower()
+ self.cert_filepath = self.host_options.get('cert_filepath')
+ self.key_filepath = self.host_options.get('key_filepath')
+ self.verify = self.host_options['validate_certs']
+ self.timeout = timeout
+ port = self.host_options['http_port']
+ self.force_ontap_version = self.host_options.get('force_ontap_version')
+ if port is None:
+ self.url = 'https://%s/api/' % self.hostname
+ else:
+ self.url = 'https://%s:%d/api/' % (self.hostname, port)
+ self.is_rest_error = None
+ self.fallback_to_zapi_reason = None
+ self.ontap_version = dict(
+ full='unknown',
+ generation=-1,
+ major=-1,
+ minor=-1,
+ valid=False
+ )
+ self.errors = []
+ self.debug_logs = []
+ self.auth_method = set_auth_method(self.module, self.username, self.password, self.cert_filepath, self.key_filepath)
+ self.check_required_library()
+ if has_feature(module, 'trace_apis'):
+ logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s')
+ self.log_headers = has_feature(module, 'trace_headers')
+ self.log_auth_args = has_feature(module, 'trace_auth_args')
+
+ def requires_ontap_9_6(self, module_name):
+ return self.requires_ontap_version(module_name)
+
+ def requires_ontap_version(self, module_name, version='9.6'):
+ suffix = " - %s" % self.is_rest_error if self.is_rest_error is not None else ""
+ return "%s only supports REST, and requires ONTAP %s or later.%s" % (module_name, version, suffix)
+
+ def options_require_ontap_version(self, options, version='9.6', use_rest=None):
+ current_version = self.get_ontap_version()
+ suffix = " - %s" % self.is_rest_error if self.is_rest_error is not None else ""
+ if current_version != (-1, -1, -1):
+ suffix += " - ONTAP version: %s.%s.%s" % current_version
+ if use_rest is not None:
+ suffix += " - using %s" % ('REST' if use_rest else 'ZAPI')
+ if isinstance(options, list) and len(options) > 1:
+ tag = "any of %s" % options
+ elif isinstance(options, list) and len(options) == 1:
+ tag = str(options[0])
+ else:
+ tag = str(options)
+ return 'using %s requires ONTAP %s or later and REST must be enabled%s.' % (tag, version, suffix)
+
+ def meets_rest_minimum_version(self, use_rest, minimum_generation, minimum_major, minimum_minor=0):
+ return use_rest and self.get_ontap_version() >= (minimum_generation, minimum_major, minimum_minor)
+
+ def fail_if_not_rest_minimum_version(self, module_name, minimum_generation, minimum_major, minimum_minor=0):
+ status_code = self.get_ontap_version_using_rest()
+ msgs = []
+ if self.use_rest == 'never':
+ msgs.append('Error: REST is required for this module, found: "use_rest: %s".' % self.use_rest)
+ # The module only supports REST, so make it required
+ self.use_rest = 'always'
+ if self.is_rest_error:
+ msgs.append('Error using REST for version, error: %s.' % self.is_rest_error)
+ if status_code != 200:
+ msgs.append('Error using REST for version, status_code: %s.' % status_code)
+ if msgs:
+ self.module.fail_json(msg=' '.join(msgs))
+ version = self.get_ontap_version()
+ if version < (minimum_generation, minimum_major, minimum_minor):
+ msg = 'Error: ' + self.requires_ontap_version(module_name, '%d.%d.%d' % (minimum_generation, minimum_major, minimum_minor))
+ msg += ' Found: %s.%s.%s.' % version
+ self.module.fail_json(msg=msg)
+
+ def check_required_library(self):
+ if not HAS_REQUESTS:
+ self.module.fail_json(msg=missing_required_lib('requests'))
+
+ def build_headers(self, accept=None, vserver_name=None, vserver_uuid=None):
+ headers = {'X-Dot-Client-App': CLIENT_APP_VERSION % self.module._name}
+ # accept is used to turn on/off HAL linking
+ if accept is not None:
+ headers['accept'] = accept
+ # vserver tunneling using vserver name and/or UUID
+ if vserver_name is not None:
+ headers['X-Dot-SVM-Name'] = vserver_name
+ if vserver_uuid is not None:
+ headers['X-Dot-SVM-UUID'] = vserver_uuid
+ return headers
+
+ def send_request(self, method, api, params, json=None, headers=None, files=None):
+ ''' send http request and process reponse, including error conditions '''
+ url = self.url + api
+
+ def get_auth_args():
+ if self.auth_method == 'single_cert':
+ kwargs = dict(cert=self.cert_filepath)
+ elif self.auth_method == 'cert_key':
+ kwargs = dict(cert=(self.cert_filepath, self.key_filepath))
+ elif self.auth_method in ('basic_auth', 'speedy_basic_auth'):
+ # with requests, there is no challenge, eg no 401.
+ kwargs = dict(auth=(self.username, self.password))
+ else:
+ raise KeyError(self.auth_method)
+ return kwargs
+
+ status_code, json_dict, error_details = self._send_request(method, url, params, json, headers, files, get_auth_args())
+
+ return status_code, json_dict, error_details
+
+ def _send_request(self, method, url, params, json, headers, files, auth_args):
+ status_code = None
+ json_dict = None
+ json_error = None
+ error_details = None
+ if headers is None:
+ headers = self.build_headers()
+
+ def fail_on_non_empty_value(response):
+ '''json() may fail on an empty value, but it's OK if no response is expected.
+ To avoid false positives, only report an issue when we expect to read a value.
+ The first get will see it.
+ '''
+ if method == 'GET' and has_feature(self.module, 'strict_json_check'):
+ contents = response.content
+ if len(contents) > 0:
+ raise ValueError("Expecting json, got: %s" % contents)
+
+ def get_json(response):
+ ''' extract json, and error message if present '''
+ try:
+ json = response.json()
+ except ValueError:
+ fail_on_non_empty_value(response)
+ return None, None
+ return json, json.get('error')
+
+ self.log_debug('sending', repr(dict(method=method, url=url, verify=self.verify, params=params,
+ timeout=self.timeout, json=json,
+ headers=headers if self.log_headers else 'redacted',
+ auth_args=auth_args if self.log_auth_args else 'redacted')))
+ try:
+ response = requests.request(method, url, verify=self.verify, params=params,
+ timeout=self.timeout, json=json, headers=headers, files=files, **auth_args)
+ status_code = response.status_code
+ self.log_debug(status_code, response.content)
+ # 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:
+ try:
+ __, json_error = get_json(response)
+ except (AttributeError, ValueError):
+ json_error = None
+ 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
+ if not error_details and not json_dict:
+ if json_dict is None:
+ json_dict = {}
+ if method == 'OPTIONS':
+ # OPTIONS provides the list of supported verbs
+ json_dict['Allow'] = response.headers.get('Allow')
+ if response.headers.get('Content-Type', '').startswith("multipart/form-data"):
+ json_dict['text'] = response.text
+ return status_code, json_dict, error_details
+
+ def _is_job_done(self, job_json, job_state, job_error, timed_out):
+ """ return (done, message, error)
+ done is True to indicate that the job is complete, or failed, or timed out
+ done is False when the job is still running
+ """
+ # a job looks like this
+ # {
+ # "uuid": "cca3d070-58c6-11ea-8c0c-005056826c14",
+ # "description": "POST /api/cluster/metrocluster",
+ # "state": "failure",
+ # "message": "There are not enough disks in Pool1.", **OPTIONAL**
+ # "code": 2432836,
+ # "start_time": "2020-02-26T10:35:44-08:00",
+ # "end_time": "2020-02-26T10:47:38-08:00",
+ # "_links": {
+ # "self": {
+ # "href": "/api/cluster/jobs/cca3d070-58c6-11ea-8c0c-005056826c14"
+ # }
+ # }
+ # }
+ done, error = False, None
+ message = job_json.get('message', '') if job_json else None
+ if job_state == 'failure':
+ # if the job has failed, return message as error
+ error = message
+ message = None
+ done = True
+ elif job_state not in ('queued', 'running', None):
+ error = job_error
+ done = True
+ elif timed_out:
+ # Would like to post a message to user (not sure how)
+ self.log_error(0, 'Timeout error: Process still running')
+ error = 'Timeout error: Process still running'
+ if job_error is not None:
+ error += ' - %s' % job_error
+ done = True
+ return done, message, error
+
+ def wait_on_job(self, job, timeout=600, increment=60):
+ try:
+ url = job['_links']['self']['href'].split('api/')[1]
+ except Exception as err:
+ self.log_error(0, 'URL Incorrect format: %s - Job: %s' % (err, job))
+ return None, 'URL Incorrect format: %s - Job: %s' % (err, job)
+ # Expecting job to be in the following format
+ # {'job':
+ # {'uuid': 'fde79888-692a-11ea-80c2-005056b39fe7',
+ # '_links':
+ # {'self':
+ # {'href': '/api/cluster/jobs/fde79888-692a-11ea-80c2-005056b39fe7'}
+ # }
+ # }
+ # }
+ error = None
+ errors = []
+ message = None
+ runtime = 0
+ retries = 0
+ max_retries = 3
+ done = False
+ while not done:
+ # Will run every <increment> seconds for <timeout> seconds
+ job_json, job_error = self.get(url, None)
+ job_state = job_json.get('state', None) if job_json else None
+ # ignore error if status is provided in the job
+ if job_error and job_state is None:
+ errors.append(str(job_error))
+ retries += 1
+ if retries > max_retries:
+ error = " - ".join(errors)
+ self.log_error(0, 'Job error: Reached max retries.')
+ done = True
+ else:
+ retries = 0
+ done, message, error = self._is_job_done(job_json, job_state, job_error, runtime >= timeout)
+ if not done:
+ time.sleep(increment)
+ runtime += increment
+ return message, error
+
+ def get(self, api, params=None, headers=None):
+ method = 'GET'
+ dummy, message, error = self.send_request(method, api, params, json=None, headers=headers)
+ return message, error
+
+ def post(self, api, body, params=None, headers=None, files=None):
+ method = 'POST'
+ retry = 3
+ while retry > 0:
+ dummy, message, error = self.send_request(method, api, params, json=body, headers=headers, files=files)
+ if error and type(error) is dict and 'temporarily locked' in error.get('message', ''):
+ time.sleep(30)
+ retry = retry - 1
+ continue
+ break
+ return message, error
+
+ def patch(self, api, body, params=None, headers=None, files=None):
+ method = 'PATCH'
+ retry = 3
+ while retry > 0:
+ dummy, message, error = self.send_request(method, api, params, json=body, headers=headers, files=files)
+ if error and type(error) is dict and 'temporarily locked' in error.get('message', ''):
+ time.sleep(30)
+ retry = retry - 1
+ continue
+ break
+ return message, error
+
+ def delete(self, api, body=None, params=None, headers=None):
+ method = 'DELETE'
+ dummy, message, error = self.send_request(method, api, params, json=body, headers=headers)
+ return message, error
+
+ def options(self, api, params=None, headers=None):
+ method = 'OPTIONS'
+ dummy, message, error = self.send_request(method, api, params, json=None, headers=headers)
+ return message, error
+
+ def set_version(self, message):
+ try:
+ version = message.get('version', 'not found')
+ except AttributeError:
+ self.ontap_version['valid'] = False
+ self.ontap_version['full'] = 'unreadable message'
+ return
+ for key in self.ontap_version:
+ try:
+ self.ontap_version[key] = version.get(key, -1)
+ except AttributeError:
+ self.ontap_version[key] = -1
+ self.ontap_version['valid'] = all(
+ self.ontap_version[key] != -1 for key in self.ontap_version if key != 'valid'
+ )
+
+ def get_ontap_version(self):
+ if self.ontap_version['valid']:
+ return self.ontap_version['generation'], self.ontap_version['major'], self.ontap_version['minor']
+ return -1, -1, -1
+
+ def get_node_version_using_rest(self):
+ # using GET rather than HEAD because the error messages are different,
+ # and we need the version as some REST options are not available in earlier versions
+ method = 'GET'
+ api = 'cluster/nodes'
+ params = {'fields': ['version']}
+ status_code, message, error = self.send_request(method, api, params=params)
+ if message and 'records' in message and len(message['records']) > 0:
+ message = message['records'][0]
+ return status_code, message, error
+
+ def get_ontap_version_from_params(self):
+ """ Provide a way to override the current version
+ This is required when running a custom vsadmin role as ONTAP does not currently allow access to /api/cluster.
+ This may also be interesting for testing :)
+ Report a warning if API call failed to report version.
+ Report a warning if current version could be fetched and is different.
+ """
+ try:
+ version = [int(x) for x in self.force_ontap_version.split('.')]
+ if len(version) == 2:
+ version.append(0)
+ gen, major, minor = version
+ except (TypeError, ValueError) as exc:
+ self.module.fail_json(
+ msg='Error: unexpected format in force_ontap_version, expecting G.M.m or G.M, as in 9.10.1, got: %s, error: %s'
+ % (self.force_ontap_version, exc))
+
+ warning = ''
+ read_version = self.get_ontap_version()
+ if read_version == (-1, -1, -1):
+ warning = ', unable to read current version:'
+ elif read_version != (gen, major, minor):
+ warning = ' but current version is %s' % self.ontap_version['full']
+ if warning:
+ warning = 'Forcing ONTAP version to %s%s' % (self.force_ontap_version, warning)
+ self.set_version({'version': {
+ 'generation': gen,
+ 'major': major,
+ 'minor': minor,
+ 'full': 'set by user to %s' % self.force_ontap_version,
+ }})
+ return warning
+
+ def get_ontap_version_using_rest(self):
+ # using GET rather than HEAD because the error messages are different,
+ # and we need the version as some REST options are not available in earlier versions
+ method = 'GET'
+ api = 'cluster'
+ params = {'fields': ['version']}
+ status_code, message, error = self.send_request(method, api, params=params)
+ try:
+ if error and 'are available in precluster.' in error.get('message', ''):
+ # in precluster mode, version is not available :(
+ status_code, message, error = self.get_node_version_using_rest()
+ except AttributeError:
+ pass
+ self.set_version(message)
+ if error:
+ self.log_error(status_code, str(error))
+ if self.force_ontap_version:
+ warning = self.get_ontap_version_from_params()
+ if error:
+ warning += ' error: %s, status_code: %s' % (error, status_code)
+ if warning:
+ self.module.warn(warning)
+ msg = 'Forcing ONTAP version to %s' % self.force_ontap_version
+ if error:
+ self.log_error('INFO', msg)
+ else:
+ self.log_debug('INFO', msg)
+ error = None
+ status_code = 200
+ self.is_rest_error = str(error) if error else None
+ return status_code
+
+ def convert_parameter_keys_to_dot_notation(self, parameters):
+ """ Get all variable set in a list and add them to a dict so that partially_supported_rest_properties works correctly """
+ if isinstance(parameters, dict):
+ temp = {}
+ for parameter in parameters:
+ if isinstance(parameters[parameter], list):
+ if parameter not in temp:
+ temp[parameter] = {}
+ for adict in parameters[parameter]:
+ if isinstance(adict, dict):
+ for key in adict:
+ temp[parameter + '.' + key] = 0
+ parameters.update(temp)
+ return parameters
+
+ def _is_rest(self, used_unsupported_rest_properties=None, partially_supported_rest_properties=None, parameters=None):
+ if self.use_rest not in ['always', 'auto', 'never']:
+ error = "use_rest must be one of: never, always, auto. Got: '%s'" % self.use_rest
+ return False, error
+ if self.use_rest == "always" and used_unsupported_rest_properties:
+ error = "REST API currently does not support '%s'" % ', '.join(used_unsupported_rest_properties)
+ return True, error
+ if self.use_rest == 'never':
+ # force ZAPI if requested
+ return False, None
+ # don't send a new request if we already know the version
+ status_code = self.get_ontap_version_using_rest() if self.get_ontap_version() == (-1, -1, -1) else 200
+ if self.use_rest == "always" and partially_supported_rest_properties:
+ # If a variable is on a list we need to move it to a dict for this check to work correctly.
+ temp_parameters = parameters.copy()
+ temp_parameters = self.convert_parameter_keys_to_dot_notation(temp_parameters)
+ error = '\n'.join(
+ "Minimum version of ONTAP for %s is %s." % (property[0], str(property[1]))
+ for property in partially_supported_rest_properties
+ if self.get_ontap_version()[:3] < property[1] and property[0] in temp_parameters
+ )
+ if error != '':
+ return True, 'Error: %s Current version: %s.' % (error, self.get_ontap_version())
+ if self.use_rest == 'always':
+ # ignore error, it will show up later when calling another REST API
+ return True, None
+ # we're now using 'auto'
+ if used_unsupported_rest_properties:
+ # force ZAPI if some parameter requires it
+ if self.get_ontap_version()[:2] > (9, 5):
+ self.fallback_to_zapi_reason =\
+ 'because of unsupported option(s) or option value(s) in REST: %s' % used_unsupported_rest_properties
+ self.module.warn('Falling back to ZAPI %s' % self.fallback_to_zapi_reason)
+ return False, None
+ if partially_supported_rest_properties:
+ # if ontap version is lower than partially_supported_rest_properties version, force ZAPI, only if the paramater is used
+ # If a variable is on a list we need to move it to a dict for this check to work correctly.
+ temp_parameters = parameters.copy()
+ temp_parameters = self.convert_parameter_keys_to_dot_notation(temp_parameters)
+ for property in partially_supported_rest_properties:
+ if self.get_ontap_version()[:3] < property[1] and property[0] in temp_parameters:
+ self.fallback_to_zapi_reason =\
+ 'because of unsupported option(s) or option value(s) "%s" in REST require %s' % (property[0], str(property[1]))
+ self.module.warn('Falling back to ZAPI %s' % self.fallback_to_zapi_reason)
+ return False, None
+ if self.get_ontap_version()[:2] in ((9, 4), (9, 5)):
+ # we can't trust REST support on 9.5, and not at all on 9.4
+ return False, None
+ return (True, None) if status_code == 200 else (False, None)
+
+ def is_rest_supported_properties(self, parameters, unsupported_rest_properties=None, partially_supported_rest_properties=None, report_error=False):
+ used_unsupported_rest_properties = None
+ if unsupported_rest_properties:
+ used_unsupported_rest_properties = [x for x in unsupported_rest_properties if x in parameters]
+ use_rest, error = self.is_rest(used_unsupported_rest_properties, partially_supported_rest_properties, parameters)
+ if report_error:
+ return use_rest, error
+ if error:
+ self.module.fail_json(msg=error)
+ return use_rest
+
+ def is_rest(self, used_unsupported_rest_properties=None, partially_supported_rest_properties=None, parameters=None):
+ ''' only return error if there is a reason to '''
+ use_rest, error = self._is_rest(used_unsupported_rest_properties, partially_supported_rest_properties, parameters)
+ if used_unsupported_rest_properties is None and partially_supported_rest_properties is None:
+ return use_rest
+ return use_rest, error
+
+ def log_error(self, status_code, message):
+ LOG.error("%s: %s", status_code, message)
+ self.errors.append(message)
+ self.debug_logs.append((status_code, message))
+
+ def log_debug(self, status_code, content):
+ LOG.debug("%s: %s", status_code, content)
+ self.debug_logs.append((status_code, content))
+
+ def write_to_file(self, tag, data=None, filepath=None, append=True):
+ '''
+ This function is only for debug purposes, all calls to write_to_file should be removed
+ before submitting.
+ If data is None, tag is considered as data
+ else tag is a label, and data is data.
+ '''
+ if filepath is None:
+ filepath = '/tmp/ontap_log'
+ mode = 'a' if append else 'w'
+ with open(filepath, mode) as afile:
+ if data is not None:
+ afile.write("%s: %s\n" % (str(tag), str(data)))
+ else:
+ afile.write(str(tag))
+ afile.write('\n')
+
+ def write_errors_to_file(self, tag=None, filepath=None, append=True):
+ if tag is None:
+ tag = 'Error'
+ for error in self.errors:
+ self.write_to_file(tag, error, filepath, append)
+ if not append:
+ append = True
+
+ def write_debug_log_to_file(self, tag=None, filepath=None, append=True):
+ if tag is None:
+ tag = 'Debug'
+ for status_code, message in self.debug_logs:
+ self.write_to_file(tag, status_code, filepath, append)
+ if not append:
+ append = True
+ self.write_to_file(tag, message, filepath, append)
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp_elementsw_module.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_elementsw_module.py
new file mode 100644
index 000000000..d16c992ec
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_elementsw_module.py
@@ -0,0 +1,41 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+HAS_SF_SDK = False
+try:
+ # pylint: disable=unused-import
+ 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 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
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp_ipaddress.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_ipaddress.py
new file mode 100644
index 000000000..a936071ca
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_ipaddress.py
@@ -0,0 +1,134 @@
+# 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) 2020-2022, 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
+
+ Provides accesss to ipaddress - mediating unicode issues with python2.7
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils._text import to_native
+
+try:
+ import ipaddress
+ HAS_IPADDRESS_LIB = True
+ IMPORT_ERROR = None
+except ImportError as exc:
+ HAS_IPADDRESS_LIB = False
+ IMPORT_ERROR = to_native(exc)
+
+
+def _check_ipaddress_is_present(module):
+ '''
+ report error at runtime rather than when attempting to load the module
+ '''
+ if HAS_IPADDRESS_LIB:
+ return None
+ module.fail_json(msg="Error: the python ipaddress package is required for this module. Import error: %s" % IMPORT_ERROR)
+
+
+def _get_ipv4orv6_address(ip_address, module):
+ '''
+ return IPV4Adress or IPV6Address object
+ '''
+ _check_ipaddress_is_present(module)
+ # python 2.7 requires unicode format
+ ip_addr = u'%s' % ip_address
+ try:
+ return ipaddress.ip_address(ip_addr)
+ except ValueError as exc:
+ error = 'Error: Invalid IP address value %s - %s' % (ip_address, to_native(exc))
+ module.fail_json(msg=error)
+
+
+def _get_ipv4orv6_network(ip_address, netmask, strict, module):
+ '''
+ return IPV4Network or IPV6Network object
+ '''
+ _check_ipaddress_is_present(module)
+ # python 2.7 requires unicode format
+ ip_addr = u'%s/%s' % (ip_address, netmask) if netmask is not None else u'%s' % ip_address
+ try:
+ return ipaddress.ip_network(ip_addr, strict)
+ except ValueError as exc:
+ error = 'Error: Invalid IP network value %s' % ip_addr
+ if 'has host bits set' in to_native(exc):
+ error += '. Please specify a network address without host bits set'
+ elif netmask is not None:
+ error += '. Check address and netmask values'
+ error += ': %s.' % to_native(exc)
+ module.fail_json(msg=error)
+
+
+def _check_ipv6_has_prefix_length(ip_address, netmask, module):
+ ip_address = _get_ipv4orv6_address(ip_address, module)
+ if not isinstance(ip_address, ipaddress.IPv6Address) or isinstance(netmask, int):
+ return
+ if ':' in netmask:
+ module.fail_json(msg='Error: only prefix_len is supported for IPv6 addresses, got %s' % netmask)
+
+
+def validate_ip_address_is_network_address(ip_address, module):
+ '''
+ Validate if the given IP address is a network address (i.e. it's host bits are set to 0)
+ ONTAP doesn't validate if the host bits are set,
+ and hence doesn't add a new address unless the IP is from a different network.
+ So this validation allows the module to be idempotent.
+ :return: None
+ '''
+ dummy = _get_ipv4orv6_network(ip_address, None, True, module)
+
+
+def validate_and_compress_ip_address(ip_address, module):
+ '''
+ 0's in IPv6 addresses can be compressed to save space
+ This will be a noop for IPv4 address
+ In addition, it makes sure the address is in a valid format
+ '''
+ # return compressed value for IPv6 and value in . notation for IPv4
+ return str(_get_ipv4orv6_address(ip_address, module))
+
+
+def netmask_length_to_netmask(ip_address, length, module):
+ '''
+ input: ip_address and netmask length
+ output: netmask in dot notation
+ '''
+ return str(_get_ipv4orv6_network(ip_address, length, False, module).netmask)
+
+
+def netmask_to_netmask_length(ip_address, netmask, module):
+ '''
+ input: ip_address and netmask in dot notation for IPv4, expanded netmask is not supported for IPv6
+ netmask as int or a str representaiton of int is also accepted
+ output: netmask length as int
+ '''
+ _check_ipv6_has_prefix_length(ip_address, netmask, module)
+ return _get_ipv4orv6_network(ip_address, netmask, False, module).prefixlen
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/netapp_module.py b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_module.py
new file mode 100644
index 000000000..91acd3933
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/netapp_module.py
@@ -0,0 +1,619 @@
+# 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 '''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from copy import deepcopy
+import re
+import traceback
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+
+ZAPI_ONLY_DEPRECATION_MESSAGE = "This module only supports ZAPI and is deprecated. "\
+ "It will no longer work with newer versions of ONTAP. "\
+ "The final ONTAP version to support ZAPI is ONTAP 9.12.1."
+
+
+def cmp(obj1, obj2):
+ """
+ Python 3 does not have a cmp function, this will do the cmp.
+ :param obj1: first object to check
+ :param obj2: second object to check
+ :return:
+ """
+ # convert to lower case for string comparison.
+ if obj1 is None:
+ return -1
+ if isinstance(obj1, str) and isinstance(obj2, str):
+ obj1 = obj1.lower()
+ obj2 = obj2.lower()
+ # if list has string element, convert string to lower case.
+ if isinstance(obj1, list) and isinstance(obj2, list):
+ obj1 = [x.lower() if isinstance(x, str) else x for x in obj1]
+ obj2 = [x.lower() if isinstance(x, str) else x for x in obj2]
+ obj1.sort()
+ obj2.sort()
+ return (obj1 > obj2) - (obj1 < obj2)
+
+
+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, module=None):
+ # we can call this with module set to self or self.module
+ # self is a NetApp module, while self.module is the AnsibleModule object
+ self.netapp_module = None
+ self.ansible_module = module
+ if module and getattr(module, 'module', None) is not None:
+ self.netapp_module = module
+ self.ansible_module = module.module
+ # When using self or self.module, this gives access to:
+ # self.ansible_module.fail_json
+ # When using self, this gives access to:
+ # self.netapp_module.rest_api.log_debug
+ self.log = []
+ self.changed = False
+ self.parameters = {'name': 'not initialized'}
+ self.zapi_string_keys = {}
+ self.zapi_bool_keys = {}
+ self.zapi_list_keys = {}
+ self.zapi_int_keys = {}
+ self.zapi_required = {}
+ self.params_to_rest_api_keys = {}
+
+ def module_deprecated(self, module):
+ module.warn(ZAPI_ONLY_DEPRECATION_MESSAGE)
+
+ def module_replaces(self, new_module, module):
+ self.module_deprecated(module)
+ module.warn('netapp.ontap.%s should be used instead.' % new_module)
+
+ def set_parameters(self, ansible_params):
+ self.parameters = {}
+ for param in ansible_params:
+ if ansible_params[param] is not None:
+ self.parameters[param] = ansible_params[param]
+ return self.parameters
+
+ def fall_back_to_zapi(self, module, msg, parameters):
+ if parameters['use_rest'].lower() == 'always':
+ module.fail_json(msg='Error: %s' % msg)
+ if parameters['use_rest'].lower() == 'auto':
+ module.warn('Falling back to ZAPI: %s' % msg)
+ return False
+
+ def check_and_set_parameters(self, module):
+ self.parameters = {}
+ check_for_none = netapp_utils.has_feature(module, 'check_required_params_for_none')
+ if check_for_none:
+ required_keys = [key for key, value in module.argument_spec.items() if value.get('required')]
+ for param in module.params:
+ if module.params[param] is not None:
+ self.parameters[param] = module.params[param]
+ elif check_for_none and param in required_keys:
+ module.fail_json(msg="%s requires a value, got: None" % param)
+ return self.parameters
+
+ @staticmethod
+ def type_error_message(type_str, key, value):
+ return "expecting '%s' type for %s: %s, got: %s" % (type_str, repr(key), repr(value), type(value))
+
+ def get_value_for_bool(self, from_zapi, value, key=None):
+ """
+ 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
+ :param key: if present, force error checking to validate type, and accepted values
+ :return: string or boolean
+ """
+ if value is None:
+ return None
+ if from_zapi:
+ if key is not None and not isinstance(value, str):
+ raise TypeError(self.type_error_message('str', key, value))
+ if key is not None and value not in ('true', 'false'):
+ raise ValueError('Unexpected value: %s received from ZAPI for boolean attribute: %s' % (repr(value), repr(key)))
+ return value == 'true'
+ if key is not None and not isinstance(value, bool):
+ raise TypeError(self.type_error_message('bool', key, value))
+ return 'true' if value else 'false'
+
+ def get_value_for_int(self, from_zapi, value, key=None):
+ """
+ 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
+ :param key: if present, force error checking to validate type
+ :return: string or integer
+ """
+ if value is None:
+ return None
+ if from_zapi:
+ if key is not None and not isinstance(value, str):
+ raise TypeError(self.type_error_message('str', key, value))
+ return int(value)
+ if key is not None and not isinstance(value, int):
+ raise TypeError(self.type_error_message('int', key, value))
+ 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 []
+ return [zapi_child.get_content() for zapi_child in zapi_parent.get_children()]
+
+ 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())
+ '''
+ desired_state = desired['state'] if 'state' in desired else '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
+ return 'create' if current is None else 'delete'
+
+ @staticmethod
+ def check_keys(current, desired):
+ ''' TODO: raise an error if keys do not match
+ with the exception of:
+ new_name, state in desired
+ '''
+
+ @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
+ '''
+ current_copy = deepcopy(current)
+ desired_copy = deepcopy(desired)
+
+ # get what in desired and not in current
+ desired_diff_list = []
+ for item in desired:
+ if item in current_copy:
+ current_copy.remove(item)
+ else:
+ desired_diff_list.append(item)
+
+ # get what in current but not in desired
+ current_diff_list = []
+ for item in current:
+ if item in desired_copy:
+ desired_copy.remove(item)
+ else:
+ current_diff_list.append(item)
+
+ if desired_diff_list or current_diff_list:
+ # there are changes
+ return desired_diff_list if get_list_diff else desired
+ else:
+ return None
+
+ 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 = {}
+ if current is None:
+ return modified
+
+ if not isinstance(desired, dict):
+ raise TypeError("Expecting dict, got: %s with current: %s" % (desired, current))
+ # error out if keys do not match
+ self.check_keys(current, desired)
+
+ # collect changed attributes
+ for key, value in current.items():
+ # if self.netapp_module:
+ # self.netapp_module.rest_api.log_debug('KDV', "%s:%s:%s" % (key, desired.get(key), value))
+ if desired.get(key) is not None:
+ modified_value = None
+ if isinstance(value, list):
+ modified_value = self.compare_lists(value, desired[key], get_list_diff) # get modified list from current and desired
+ elif isinstance(value, dict):
+ modified_value = self.get_modified_attributes(value, desired[key]) or None
+ else:
+ try:
+ result = cmp(value, desired[key])
+ except TypeError as exc:
+ raise TypeError("%s, key: %s, value: %s, desired: %s" % (repr(exc), key, repr(value), repr(desired[key])))
+ # if self.netapp_module:
+ # self.netapp_module.rest_api.log_debug('RESULT', result)
+ if result != 0:
+ modified_value = desired[key]
+ if modified_value is not None:
+ modified[key] = modified_value
+
+ 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
+
+ I'm not sure we need this function any more.
+ I think a better way to do it is to:
+ 1. look if a create is required (eg the target resource does not exist and state==present)
+ 2. consider that a create can be fullfilled by different actions: rename, create from scratch, move, ...
+ So for rename:
+ cd_action = self.na_helper.get_cd_action(current, self.parameters)
+ if cd_action == 'create' and self.parameters.get('from_name'):
+ # creating new subnet by renaming
+ current = self.get_subnet(self.parameters['from_name'])
+ if current is None:
+ self.module.fail_json(msg="Error renaming: subnet %s does not exist" %
+ self.parameters['from_name'])
+ rename = True
+ cd_action = None
+ '''
+ if source is None and target is None:
+ # error, do nothing
+ # cannot rename a non existent resource
+ return None
+ if target is None:
+ # source is not None and target is None:
+ # rename is in order
+ self.changed = True
+ return True
+ # target is not None, so do nothing as the destination exists
+ # if source is None, maybe we already renamed
+ # if source is not None, maybe a new resource was created after being renamed
+ return False
+
+ @staticmethod
+ def sanitize_wwn(initiator):
+ ''' igroup initiator may or may not be using WWN format: eg 20:00:00:25:B5:00:20:01
+ if format is matched, convert initiator to lowercase, as this is what ONTAP is using '''
+ wwn_format = r'[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){7}'
+ initiator = initiator.strip()
+ if re.match(wwn_format, initiator):
+ initiator = initiator.lower()
+ return initiator
+
+ def safe_get(self, an_object, key_list, allow_sparse_dict=True):
+ ''' recursively traverse a dictionary or a any object supporting get_item or indexing
+ (in our case, python dicts and NAElement responses, and lists)
+ It is expected that some keys can be missing, this is controlled with allow_sparse_dict
+
+ return value if the key chain is exhausted
+ return None if a key is not found and allow_sparse_dict is True
+ raise KeyError is a key is not found and allow_sparse_dict is False (looking for exact match)
+ raise TypeError if an intermediate element cannot be indexed,
+ unless the element is None and allow_sparse_dict is True
+ '''
+ if not key_list:
+ # we've exhausted the keys, good!
+ return an_object
+ key_list = list(key_list) # preserve original values
+ key = key_list.pop(0)
+ try:
+ return self.safe_get(an_object[key], key_list, allow_sparse_dict=allow_sparse_dict)
+ except (KeyError, IndexError) as exc:
+ # error, key or index not found
+ if allow_sparse_dict:
+ return None
+ raise exc
+ except TypeError as exc:
+ # error, we were expecting a dict or NAElement
+ if allow_sparse_dict and an_object is None:
+ return None
+ raise exc
+
+ def convert_value(self, value, convert_to):
+ if convert_to is None:
+ return value, None
+ if not isinstance(value, str):
+ return None, ('Unexpected type: %s for %s' % (type(value), str(value)))
+ if convert_to == str:
+ return value, None
+ if convert_to == int:
+ try:
+ return int(value), None
+ except ValueError as exc:
+ return None, ('Unexpected value for int: %s, %s' % (str(value), str(exc)))
+ if convert_to == bool:
+ if value not in ('true', 'false'):
+ return None, 'Unexpected value: %s received from ZAPI for boolean attribute' % value
+ return value == 'true', None
+ if convert_to == 'bool_online':
+ return value == 'online', None
+ self.ansible_module.fail_json(msg='Error: Unexpected value for convert_to: %s' % convert_to)
+
+ def zapi_get_value(self, na_element, key_list, required=False, default=None, convert_to=None):
+ """ read a value from na_element using key_list
+
+ If required is True, an error is reported if a key in key_list is not found.
+ If required is False and the value is not found, uses default as the value.
+ If convert_to is set to str, bool, int, the ZAPI value is converted from str to the desired type.
+ suported values: None, the python types int, str, bool, special 'bool_online'
+
+ Errors: fail_json is called for:
+ - a key is not found and required=True,
+ - a format conversion error
+ """
+
+ # keep a copy, as the list is mutated
+ saved_key_list = list(key_list)
+ try:
+ value = self.safe_get(na_element, key_list, allow_sparse_dict=not required)
+ except (KeyError, TypeError) as exc:
+ error = exc
+ else:
+ value, error = self.convert_value(value, convert_to) if value is not None else (default, None)
+ if error:
+ self.ansible_module.fail_json(msg='Error reading %s from %s: %s' % (saved_key_list, na_element.to_string(), error))
+ return value
+
+ def zapi_get_attrs(self, na_element, attr_dict, result):
+ """ Retrieve a list of attributes from na_elements
+ see na_ontap_volume for an example.
+ na_element: xml element as returned by ZAPI.
+ attr_dict:
+ A dict of dict, with format:
+ key: dict(key_list, required=False, default=None, convert_to=None, omitnone=False)
+ The keys are used to index a result dictionary, values are read from a ZAPI object indexed by key_list.
+ If required is True, an error is reported if a key in key_list is not found.
+ If required is False and the value is not found, uses default as the value.
+ If convert_to is set to str, bool, int, the ZAPI value is converted from str to the desired type.
+ I'm not sure there is much value in omitnone, but it preserves backward compatibility.
+ When the value is None, if omitnone is False, a None value is recorded, if True, the key is not set.
+ result: an existing dictionary. keys are added or updated based on attrs.
+
+ Errors: fail_json is called for:
+ - a key is not found and required=True,
+ - a format conversion error
+ """
+ for key, kwargs in attr_dict.items():
+ omitnone = kwargs.pop('omitnone', False)
+ value = self.zapi_get_value(na_element, **kwargs)
+ if value is not None or not omitnone:
+ result[key] = value
+
+ def _filter_out_none_entries_from_dict(self, adict, allow_empty_list_or_dict):
+ """take a dict as input and return a dict without keys whose values are None
+ return empty dicts or lists if allow_empty_list_or_dict otherwise skip empty dicts or lists.
+ """
+ result = {}
+ for key, value in adict.items():
+ if isinstance(value, (list, dict)):
+ sub = self.filter_out_none_entries(value, allow_empty_list_or_dict)
+ if sub or allow_empty_list_or_dict:
+ # allow empty dict or list if allow_empty_list_or_dict is set.
+ # skip empty dict or list otherwise
+ result[key] = sub
+ elif value is not None:
+ # skip None value
+ result[key] = value
+ return result
+
+ def _filter_out_none_entries_from_list(self, alist, allow_empty_list_or_dict):
+ """take a list as input and return a list without elements whose values are None
+ return empty dicts or lists if allow_empty_list_or_dict otherwise skip empty dicts or lists.
+ """
+ result = []
+ for item in alist:
+ if isinstance(item, (list, dict)):
+ sub = self.filter_out_none_entries(item, allow_empty_list_or_dict)
+ if sub or allow_empty_list_or_dict:
+ # allow empty dict or list if allow_empty_list_or_dict is set.
+ # skip empty dict or list otherwise
+ result.append(sub)
+ elif item is not None:
+ # skip None value
+ result.append(item)
+ return result
+
+ def filter_out_none_entries(self, list_or_dict, allow_empty_list_or_dict=False):
+ """take a dict or list as input and return a dict/list without keys/elements whose values are None
+ return empty dicts or lists if allow_empty_list_or_dict otherwise skip empty dicts or lists.
+ """
+
+ if isinstance(list_or_dict, dict):
+ return self._filter_out_none_entries_from_dict(list_or_dict, allow_empty_list_or_dict)
+
+ if isinstance(list_or_dict, list):
+ return self._filter_out_none_entries_from_list(list_or_dict, allow_empty_list_or_dict)
+
+ raise TypeError('unexpected type %s' % type(list_or_dict))
+
+ @staticmethod
+ def get_caller(depth):
+ '''return the name of:
+ our caller if depth is 1
+ the caller of our caller if depth is 2
+ the caller of the caller of our caller if depth is 3
+ ...
+ '''
+ # one more caller in the stack
+ depth += 1
+ frames = traceback.extract_stack(limit=depth)
+ try:
+ function_name = frames[0].name
+ except AttributeError:
+ # python 2.7 does not have named attributes for frames
+ try:
+ function_name = frames[0][2]
+ except Exception as exc: # pylint: disable=broad-except
+ function_name = 'Error retrieving function name: %s - %s' % (str(exc), repr(frames))
+ return function_name
+
+ def fail_on_error(self, error, api=None, stack=False, depth=1, previous_errors=None):
+ '''depth identifies how far is the caller in the call stack'''
+ if error is None:
+ return
+ # one more caller to account for this function
+ depth += 1
+ if api is not None:
+ error = 'calling api: %s: %s' % (api, error)
+ results = dict(msg='Error in %s: %s' % (self.get_caller(depth), error))
+ if stack:
+ results['stack'] = traceback.format_stack()
+ if previous_errors:
+ results['previous_errors'] = ' - '.join(previous_errors)
+ if getattr(self, 'ansible_module', None) is not None:
+ self.ansible_module.fail_json(**results)
+ raise AttributeError('Expecting self.ansible_module to be set when reporting %s' % repr(results))
+
+ def compare_chmod_value(self, current_permissions, desired_permissions):
+ """
+ compare current unix_permissions to desired unix_permissions.
+ :return: True if the same, False it not the same or desired unix_permissions is not valid.
+ """
+ if current_permissions is None:
+ return False
+ if desired_permissions.isdigit():
+ return int(current_permissions) == int(desired_permissions)
+ # ONTAP will throw error as invalid field if the length is not 9 or 12.
+ if len(desired_permissions) not in [12, 9]:
+ return False
+ desired_octal_value = ''
+ # if the length is 12, first three character sets userid('s'), groupid('s') and sticky('t') attributes
+ if len(desired_permissions) == 12:
+ if desired_permissions[0] not in ['s', '-'] or desired_permissions[1] not in ['s', '-']\
+ or desired_permissions[2] not in ['t', '-']:
+ return False
+ desired_octal_value += str(self.char_to_octal(desired_permissions[:3]))
+ # if the len is 9, start from 0 else start from 3.
+ start_range = len(desired_permissions) - 9
+ for i in range(start_range, len(desired_permissions), 3):
+ if desired_permissions[i] not in ['r', '-'] or desired_permissions[i + 1] not in ['w', '-']\
+ or desired_permissions[i + 2] not in ['x', '-']:
+ return False
+ group_permission = self.char_to_octal(desired_permissions[i:i + 3])
+ desired_octal_value += str(group_permission)
+ return int(current_permissions) == int(desired_octal_value)
+
+ def char_to_octal(self, chars):
+ """
+ :param chars: Characters to be converted into octal values.
+ :return: octal value of the individual group permission.
+ """
+ total = 0
+ if chars[0] in ['r', 's']:
+ total += 4
+ if chars[1] in ['w', 's']:
+ total += 2
+ if chars[2] in ['x', 't']:
+ total += 1
+ return total
+
+ def ignore_missing_vserver_on_delete(self, error, vserver_name=None):
+ """ When a resource is expected to be absent, it's OK if the containing vserver is also absent.
+ This function expects self.parameters('vserver') to be set or the vserver_name argument to be passed.
+ error is an error returned by rest_generic.get_xxxx.
+ """
+ if self.parameters.get('state') != 'absent':
+ return False
+ if vserver_name is None:
+ if self.parameters.get('vserver') is None:
+ self.ansible_module.fail_json(
+ msg='Internal error, vserver name is required, when processing error: %s' % error, exception=traceback.format_exc())
+ vserver_name = self.parameters['vserver']
+ if isinstance(error, str):
+ pass
+ elif isinstance(error, dict):
+ if 'message' in error:
+ error = error['message']
+ else:
+ self.ansible_module.fail_json(
+ msg='Internal error, error should contain "message" key, found: %s' % error, exception=traceback.format_exc())
+ else:
+ self.ansible_module.fail_json(
+ msg='Internal error, error should be str or dict, found: %s, %s' % (type(error), error), exception=traceback.format_exc())
+ return 'SVM "%s" does not exist.' % vserver_name in error
+
+ def remove_hal_links(self, records):
+ """ Remove all _links entries """
+ if isinstance(records, dict):
+ records.pop('_links', None)
+ for record in records.values():
+ self.remove_hal_links(record)
+ if isinstance(records, list):
+ for record in records:
+ self.remove_hal_links(record)
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_application.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_application.py
new file mode 100644
index 000000000..d96f23031
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_application.py
@@ -0,0 +1,180 @@
+# 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) 2020-2022, 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
+
+ Provides accesss to application resources using REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+class RestApplication():
+ """Helper methods to manage application and application components"""
+ def __init__(self, rest_api, svm_name, app_name):
+ self.svm_name = svm_name
+ self.app_name = app_name
+ self.app_uuid = None
+ self.rest_api = rest_api
+
+ def _set_application_uuid(self):
+ """Use REST application/applications to get application uuid"""
+ api = 'application/applications'
+ query = {'svm.name': self.svm_name, 'name': self.app_name}
+ record, error = rest_generic.get_one_record(self.rest_api, api, query)
+ if error is None and record is not None:
+ self.app_uuid = record['uuid']
+ return None, error
+
+ def get_application_uuid(self):
+ """Use REST application/applications to get application uuid"""
+ error = None
+ if self.app_uuid is None:
+ dummy, error = self._set_application_uuid()
+ return self.app_uuid, error
+
+ def get_application_details(self, template=None):
+ """Use REST application/applications to get application details"""
+ uuid, error = self.get_application_uuid()
+ if error:
+ return uuid, error
+ if uuid is None: # not found
+ return None, None
+ query = dict(fields='name,%s,statistics' % template) if template else None
+ api = 'application/applications/%s' % uuid
+ return rest_generic.get_one_record(self.rest_api, api, query)
+
+ def create_application(self, body):
+ """Use REST application/applications san template to create one or more LUNs"""
+ dummy, error = self.fail_if_uuid('create_application')
+ if error is not None:
+ return dummy, error
+ api = 'application/applications'
+ query = {'return_records': 'true'}
+ response, error = rest_generic.post_async(self.rest_api, api, body, query)
+ if error and 'Unexpected argument' in error and 'exclude_aggregates' in error:
+ error += ' "exclude_aggregates" requires ONTAP 9.9.1 GA or later.'
+ return response, error
+
+ def patch_application(self, body):
+ """Use REST application/applications san template to add one or more LUNs"""
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ api = 'application/applications'
+ query = {'return_records': 'true'}
+ return rest_generic.patch_async(self.rest_api, api, self.app_uuid, body, query)
+
+ def create_application_body(self, template_name, template_body, smart_container=True):
+ if not isinstance(smart_container, bool):
+ error = "expecting bool value for smart_container, got: %s" % smart_container
+ return None, error
+ body = {
+ 'name': self.app_name,
+ 'svm': {'name': self.svm_name},
+ 'smart_container': smart_container,
+ template_name: template_body
+ }
+ return body, None
+
+ def delete_application(self):
+ """Use REST application/applications to delete app"""
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ api = 'application/applications'
+ response, error = rest_generic.delete_async(self.rest_api, api, self.app_uuid)
+ self.app_uuid = None
+ return response, error
+
+ def get_application_components(self):
+ """Use REST application/applications to get application components"""
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ api = 'application/applications/%s/components' % self.app_uuid
+ return rest_generic.get_0_or_more_records(self.rest_api, api)
+
+ def get_application_component_uuid(self):
+ """Use REST application/applications to get component uuid
+ Assume a single component per application
+ """
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ api = 'application/applications/%s/components' % self.app_uuid
+ record, error = rest_generic.get_one_record(self.rest_api, api, fields='uuid')
+ if error is None and record is not None:
+ return record['uuid'], None
+ return None, error
+
+ def get_application_component_details(self, comp_uuid=None):
+ """Use REST application/applications to get application components"""
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ if comp_uuid is None:
+ # assume a single component
+ comp_uuid, error = self.get_application_component_uuid()
+ if error:
+ return comp_uuid, error
+ if comp_uuid is None:
+ error = 'no component for application %s' % self.app_name
+ return None, error
+ api = 'application/applications/%s/components/%s' % (self.app_uuid, comp_uuid)
+ return rest_generic.get_one_record(self.rest_api, api)
+
+ def get_application_component_backing_storage(self):
+ """Use REST application/applications to get component uuid.
+
+ Assume a single component per application
+ """
+ dummy, error = self.fail_if_no_uuid()
+ if error is not None:
+ return dummy, error
+ response, error = self.get_application_component_details()
+ if error or response is None:
+ return response, error
+ return response['backing_storage'], None
+
+ def fail_if_no_uuid(self):
+ """Prevent a logic error."""
+ if self.app_uuid is None:
+ msg = 'function should not be called before application uuid is set.'
+ return None, msg
+ return None, None
+
+ def fail_if_uuid(self, fname):
+ """Prevent a logic error."""
+ if self.app_uuid is not None:
+ msg = 'function %s should not be called when application uuid is set: %s.' % (fname, self.app_uuid)
+ return None, msg
+ return None, None
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_generic.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_generic.py
new file mode 100644
index 000000000..4c570f8d8
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_generic.py
@@ -0,0 +1,101 @@
+# 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) 2021, 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 functions for NetApp ansible modules
+
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh
+
+
+def build_query_with_fields(query, fields):
+ ''' for GET requests'''
+ if fields is not None and query is None:
+ query = {}
+ if fields is not None:
+ query['fields'] = fields
+ return query
+
+
+def build_query_with_timeout(query, timeout):
+ ''' for POST, PATCH, DELETE requests'''
+ params = {} if query else None
+ if timeout > 0:
+ # without return_timeout, REST returns immediately with a 202 and a job link
+ # but the job status is 'running'
+ # with return_timeout, REST returns quickly with a 200 and a job link
+ # and the job status is 'success'
+ params = dict(return_timeout=timeout)
+ if query is not None:
+ params.update(query)
+ return params
+
+
+def get_one_record(rest_api, api, query=None, fields=None):
+ query = build_query_with_fields(query, fields)
+ response, error = rest_api.get(api, query)
+ record, error = rrh.check_for_0_or_1_records(api, response, error, query)
+ return record, error
+
+
+def get_0_or_more_records(rest_api, api, query=None, fields=None):
+ query = build_query_with_fields(query, fields)
+ response, error = rest_api.get(api, query)
+ records, error = rrh.check_for_0_or_more_records(api, response, error)
+ return records, error
+
+
+def post_async(rest_api, api, body, query=None, timeout=30, job_timeout=30, headers=None, raw_error=False, files=None):
+ # see delete_async for async and sync operations and status codes
+ response, error = rest_api.post(api, body=body, params=build_query_with_timeout(query, timeout), headers=headers, files=files)
+ # limit the polling interval to something between 5 seconds and 60 seconds
+ increment = min(max(job_timeout / 6, 5), 60)
+ response, error = rrh.check_for_error_and_job_results(api, response, error, rest_api, increment=increment, timeout=job_timeout, raw_error=raw_error)
+ return response, error
+
+
+def patch_async(rest_api, api, uuid_or_name, body, query=None, timeout=30, job_timeout=30, headers=None, raw_error=False, files=None):
+ # cluster does not use uuid or name, and query based PATCH does not use UUID (for restit)
+ api = '%s/%s' % (api, uuid_or_name) if uuid_or_name is not None else api
+ response, error = rest_api.patch(api, body=body, params=build_query_with_timeout(query, timeout), headers=headers, files=files)
+ increment = min(max(job_timeout / 6, 5), 60)
+ response, error = rrh.check_for_error_and_job_results(api, response, error, rest_api, increment=increment, timeout=job_timeout, raw_error=raw_error)
+ return response, error
+
+
+def delete_async(rest_api, api, uuid, query=None, body=None, timeout=30, job_timeout=30, headers=None, raw_error=False):
+ # query based DELETE does not use UUID (for restit)
+ api = '%s/%s' % (api, uuid) if uuid is not None else api
+ response, error = rest_api.delete(api, body=body, params=build_query_with_timeout(query, timeout), headers=headers)
+ increment = min(max(job_timeout / 6, 5), 60)
+ response, error = rrh.check_for_error_and_job_results(api, response, error, rest_api, increment=increment, timeout=job_timeout, raw_error=raw_error)
+ return response, error
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_owning_resource.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_owning_resource.py
new file mode 100644
index 000000000..597c02d50
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_owning_resource.py
@@ -0,0 +1,26 @@
+""" Support functions for NetApp ansible modules
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+def get_export_policy_id(rest_api, policy_name, svm_name, module):
+ api = 'protocols/nfs/export-policies'
+ query = {'name': policy_name, 'svm.name': svm_name}
+ record, error = rest_generic.get_one_record(rest_api, api, query)
+ if error:
+ module.fail_json(msg='Could not find export policy %s on SVM %s' % (policy_name, svm_name))
+ return record['id'] if record else None
+
+
+def get_volume_uuid(rest_api, volume_name, svm_name, module):
+ api = 'storage/volumes'
+ query = {'name': volume_name, 'svm.name': svm_name}
+ record, error = rest_generic.get_one_record(rest_api, api, query)
+ if error:
+ module.fail_json(msg='Could not find volume %s on SVM %s' % (volume_name, svm_name))
+ return record['uuid'] if record else None
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_response_helpers.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_response_helpers.py
new file mode 100644
index 000000000..59e04b3b3
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_response_helpers.py
@@ -0,0 +1,137 @@
+# 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) 2020, 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 functions for NetApp ansible modules
+
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def api_error(api, error):
+ """format error message for api error, if error is present"""
+ return "calling: %s: got %s." % (api, error) if error is not None else None
+
+
+def no_response_error(api, response):
+ """format error message for empty response"""
+ return "calling: %s: no response %s." % (api, repr(response))
+
+
+def job_error(response, error):
+ """format error message for job error"""
+ return "job reported error: %s, received %s." % (error, repr(response))
+
+
+def unexpected_response_error(api, response, query=None):
+ """format error message for reponse not matching expectations"""
+ msg = "calling: %s: unexpected response %s." % (api, repr(response))
+ if query:
+ msg += " for query: %s" % repr(query)
+ return response, msg
+
+
+def get_num_records(response):
+ """ num_records is not always present
+ if absent, count the records or assume 1
+ """
+ if 'num_records' in response:
+ return response['num_records']
+ return len(response['records']) if 'records' in response else 1
+
+
+def check_for_0_or_1_records(api, response, error, query=None):
+ """return None if no record was returned by the API
+ return record if one record was returned by the API
+ return error otherwise (error, no response, more than 1 record)
+ """
+ if error:
+ return (None, api_error(api, error)) if api else (None, error)
+ if not response:
+ return None, no_response_error(api, response)
+ num_records = get_num_records(response)
+ if num_records == 0:
+ return None, None # not found
+ if num_records != 1:
+ return unexpected_response_error(api, response, query)
+ if 'records' in response:
+ return response['records'][0], None
+ return response, None
+
+
+def check_for_0_or_more_records(api, response, error):
+ """return None if no record was returned by the API
+ return records if one or more records was returned by the API
+ return error otherwise (error, no response)
+ """
+ if error:
+ return (None, api_error(api, error)) if api else (None, error)
+ if not response:
+ return None, no_response_error(api, response)
+ if get_num_records(response) == 0:
+ return None, None # not found
+ if 'records' in response:
+ return response['records'], None
+ error = 'No "records" key in %s' % response
+ return (None, api_error(api, error)) if api else (None, error)
+
+
+def check_for_error_and_job_results(api, response, error, rest_api, **kwargs):
+ """report first error if present
+ otherwise call wait_on_job and retrieve job response or error
+ """
+ format_error = not kwargs.pop('raw_error', False)
+ if error:
+ if format_error:
+ error = api_error(api, error)
+ # we expect two types of response
+ # a plain response, for synchronous calls
+ # or a job response, for asynchronous calls
+ # and it's possible to expect both when 'return_timeout' > 0
+ #
+ # when using a query instead of UUID, REST return jobs (a list of jobs) rather than a single job
+ # only restit can send a query, all other calls are using a UUID.
+ elif isinstance(response, dict):
+ job = None
+ if 'job' in response:
+ job = response['job']
+ elif 'jobs' in response:
+ if response['num_records'] > 1:
+ error = "multiple jobs in progress, can't check status"
+ else:
+ job = response['jobs'][0]
+ if job:
+ job_response, error = rest_api.wait_on_job(job, **kwargs)
+ if error:
+ if format_error:
+ error = job_error(response, error)
+ else:
+ response['job_response'] = job_response
+ return response, error
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_user.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_user.py
new file mode 100644
index 000000000..b7c2e66a2
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_user.py
@@ -0,0 +1,49 @@
+# 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) 2021, 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 functions for NetApp ansible modules
+
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible_collections.netapp.ontap.plugins.module_utils.rest_response_helpers as rrh
+
+
+def get_users(rest_api, parameters, fields=None):
+ api = 'security/accounts'
+ query = dict()
+ for field in parameters:
+ query[field] = parameters[field]
+ if fields is not None:
+ query['fields'] = fields
+ response, error = rest_api.get(api, query)
+ users, error = rrh.check_for_0_or_more_records(api, response, error)
+ return users, error
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_volume.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_volume.py
new file mode 100644
index 000000000..10af36754
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_volume.py
@@ -0,0 +1,61 @@
+# 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) 2020, 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 functions for NetApp ansible modules
+
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.netapp.ontap.plugins.module_utils import rest_generic
+
+
+def get_volumes(rest_api, vserver=None, name=None):
+ api = 'storage/volumes'
+ query = {}
+ if vserver is not None:
+ query['svm.name'] = vserver
+ if name is not None:
+ query['name'] = name
+ if not query:
+ query = None
+ return rest_generic.get_0_or_more_records(rest_api, api, query)
+
+
+def get_volume(rest_api, vserver, name, fields=None):
+ api = 'storage/volumes'
+ query = dict(name=name)
+ query['svm.name'] = vserver
+ return rest_generic.get_one_record(rest_api, api, query, fields=fields)
+
+
+def patch_volume(rest_api, uuid, body, query=None, job_timeout=120):
+ api = 'storage/volumes'
+ return rest_generic.patch_async(rest_api, api, uuid, body, query=query, job_timeout=job_timeout)
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/rest_vserver.py b/ansible_collections/netapp/ontap/plugins/module_utils/rest_vserver.py
new file mode 100644
index 000000000..cbdfdaef9
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/rest_vserver.py
@@ -0,0 +1,61 @@
+# 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) 2021, 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 functions for NetApp ansible modules
+
+ Provides common processing for responses and errors from REST calls
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible_collections.netapp.ontap.plugins.module_utils.rest_generic import get_one_record
+
+
+def get_vserver(rest_api, name, fields=None):
+ api = 'svm/svms'
+ query = {'name': name}
+ if fields is not None:
+ query['fields'] = fields
+ vserver, error = get_one_record(rest_api, api, query)
+ return vserver, error
+
+
+def get_vserver_uuid(rest_api, name, module=None, error_on_none=False):
+ """ returns a tuple (uuid, error)
+ when module is set and an error is found, fails the module and exit
+ when error_on_none IS SET, force an error if vserver is not found
+ """
+ record, error = get_vserver(rest_api, name, 'uuid')
+ if error and module:
+ module.fail_json(msg="Error fetching vserver %s: %s" % (name, error))
+ if not error and record is None and error_on_none:
+ error = "vserver %s does not exist or is not a data vserver." % name
+ if module:
+ module.fail_json(msg="Error %s" % error)
+ return record['uuid'] if not error and record else None, error
diff --git a/ansible_collections/netapp/ontap/plugins/module_utils/zapis_svm.py b/ansible_collections/netapp/ontap/plugins/module_utils/zapis_svm.py
new file mode 100644
index 000000000..71a3f2496
--- /dev/null
+++ b/ansible_collections/netapp/ontap/plugins/module_utils/zapis_svm.py
@@ -0,0 +1,133 @@
+# 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) 2020, 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
+
+ Provides accesss to SVM (vserver) resources using ZAPI calls
+'''
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import traceback
+
+from ansible.module_utils._text import to_native
+import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils
+
+
+def get_vserver(svm_cx, vserver_name):
+ """
+ Return vserver information.
+
+ :return:
+ vserver object if vserver found
+ None if vserver is not found
+ :rtype: object/None
+ """
+ vserver_info = netapp_utils.zapi.NaElement('vserver-get-iter')
+ query_details = netapp_utils.zapi.NaElement.create_node_with_children(
+ 'vserver-info', **{'vserver-name': vserver_name})
+
+ query = netapp_utils.zapi.NaElement('query')
+ query.add_child_elem(query_details)
+ vserver_info.add_child_elem(query)
+
+ result = svm_cx.invoke_successfully(vserver_info, enable_tunneling=False)
+ vserver_details = None
+ if result.get_child_by_name('num-records') and int(result.get_child_content('num-records')) >= 1:
+ attributes_list = result.get_child_by_name('attributes-list')
+ vserver_info = attributes_list.get_child_by_name('vserver-info')
+ aggr_list = []
+ # vserver aggr-list can be empty by default
+ get_list = vserver_info.get_child_by_name('aggr-list')
+ if get_list is not None:
+ aggregates = get_list.get_children()
+ aggr_list.extend(aggr.get_content() for aggr in aggregates)
+ protocols = []
+ # allowed-protocols is not empty for data SVM, but is for node SVM
+ allowed_protocols = vserver_info.get_child_by_name('allowed-protocols')
+ if allowed_protocols is not None:
+ get_protocols = allowed_protocols.get_children()
+ protocols.extend(protocol.get_content() for protocol in get_protocols)
+ vserver_details = {'name': vserver_info.get_child_content('vserver-name'),
+ 'root_volume': vserver_info.get_child_content('root-volume'),
+ 'root_volume_aggregate': vserver_info.get_child_content('root-volume-aggregate'),
+ 'root_volume_security_style': vserver_info.get_child_content('root-volume-security-style'),
+ 'subtype': vserver_info.get_child_content('vserver-subtype'),
+ 'aggr_list': aggr_list,
+ 'language': vserver_info.get_child_content('language'),
+ 'quota_policy': vserver_info.get_child_content('quota-policy'),
+ 'snapshot_policy': vserver_info.get_child_content('snapshot-policy'),
+ 'allowed_protocols': protocols,
+ 'ipspace': vserver_info.get_child_content('ipspace'),
+ 'comment': vserver_info.get_child_content('comment'),
+ 'max_volumes': vserver_info.get_child_content('max-volumes')}
+
+ return vserver_details
+
+
+def modify_vserver(svm_cx, module, name, modify, parameters=None):
+ '''
+ Modify vserver.
+ :param name: vserver name
+ :param modify: list of modify attributes
+ :param parameters: customer original inputs
+ modify only contains the difference between the customer inputs and current
+ for some attributes, it may be safer to apply the original inputs
+ '''
+ if parameters is None:
+ parameters = modify
+
+ vserver_modify = netapp_utils.zapi.NaElement('vserver-modify')
+ vserver_modify.add_new_child('vserver-name', name)
+ for attribute in modify:
+ if attribute == 'comment':
+ vserver_modify.add_new_child('comment', parameters['comment'])
+ if attribute == 'language':
+ vserver_modify.add_new_child('language', parameters['language'])
+ if attribute == 'quota_policy':
+ vserver_modify.add_new_child('quota-policy', parameters['quota_policy'])
+ if attribute == 'snapshot_policy':
+ vserver_modify.add_new_child('snapshot-policy', parameters['snapshot_policy'])
+ if attribute == 'max_volumes':
+ vserver_modify.add_new_child('max-volumes', parameters['max_volumes'])
+ if attribute == 'allowed_protocols':
+ allowed_protocols = netapp_utils.zapi.NaElement('allowed-protocols')
+ for protocol in parameters['allowed_protocols']:
+ allowed_protocols.add_new_child('protocol', protocol)
+ vserver_modify.add_child_elem(allowed_protocols)
+ if attribute == 'aggr_list':
+ aggregates = netapp_utils.zapi.NaElement('aggr-list')
+ for aggr in parameters['aggr_list']:
+ aggregates.add_new_child('aggr-name', aggr)
+ vserver_modify.add_child_elem(aggregates)
+ try:
+ svm_cx.invoke_successfully(vserver_modify, enable_tunneling=False)
+ except netapp_utils.zapi.NaApiError as exc:
+ module.fail_json(msg='Error modifying SVM %s: %s' % (name, to_native(exc)),
+ exception=traceback.format_exc())