diff --git a/lib/ansible/module_utils/netapp.py b/lib/ansible/module_utils/netapp.py index 40bd6bb1ad..d60a91bd1a 100644 --- a/lib/ansible/module_utils/netapp.py +++ b/lib/ansible/module_utils/netapp.py @@ -33,6 +33,9 @@ from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.urls import open_url from ansible.module_utils.api import basic_auth_argument_spec +import os +import ssl + HAS_NETAPP_LIB = False try: from netapp_lib.api.zapi import zapi @@ -81,7 +84,9 @@ def na_ontap_host_argument_spec(): hostname=dict(required=True, type='str'), username=dict(required=True, type='str', aliases=['user']), password=dict(required=True, type='str', aliases=['pass'], no_log=True), - https=dict(required=False, type='bool', default=False) + https=dict(required=False, type='bool', default=False), + validate_certs=dict(required=False, type='bool', default=True), + http_port=dict(required=False, type='int') ) @@ -114,6 +119,8 @@ def setup_na_ontap_zapi(module, vserver=None): username = module.params['username'] password = module.params['password'] https = module.params['https'] + validate_certs = module.params['validate_certs'] + port = module.params['http_port'] if HAS_NETAPP_LIB: # set up zapi @@ -123,14 +130,22 @@ def setup_na_ontap_zapi(module, vserver=None): if vserver: server.set_vserver(vserver) # Todo : Replace hard-coded values with configurable parameters. - server.set_api_version(major=1, minor=21) + server.set_api_version(major=1, minor=110) # default is HTTP - if https is True: - server.set_port(443) - server.set_transport_type('HTTPS') + if https: + if port is None: + port = 443 + transport_type = 'HTTPS' + # HACK to bypass certificate verification + if validate_certs is True: + if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None): + ssl._create_default_https_context = ssl._create_unverified_context else: - server.set_port(80) - server.set_transport_type('HTTP') + if port is None: + port = 80 + transport_type = 'HTTP' + server.set_transport_type(transport_type) + server.set_port(port) server.set_server_type('FILER') return server else: @@ -150,7 +165,7 @@ def setup_ontap_zapi(module, vserver=None): if vserver: server.set_vserver(vserver) # Todo : Replace hard-coded values with configurable parameters. - server.set_api_version(major=1, minor=21) + server.set_api_version(major=1, minor=110) server.set_port(80) server.set_server_type('FILER') server.set_transport_type('HTTP') diff --git a/lib/ansible/module_utils/netapp_elementsw_module.py b/lib/ansible/module_utils/netapp_elementsw_module.py new file mode 100644 index 0000000000..b4fea3c88d --- /dev/null +++ b/lib/ansible/module_utils/netapp_elementsw_module.py @@ -0,0 +1,156 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. + +HAS_SF_SDK = False +try: + import solidfire.common + + HAS_SF_SDK = True +except: + HAS_SF_SDK = False + + +def has_sf_sdk(): + return HAS_SF_SDK + + +class NaElementSWModule(object): + + def __init__(self, elem): + self.elem_connect = elem + self.parameters = dict() + + def get_volume(self, volume_id): + """ + Return volume details if volume exists for given volume_id + + :param volume_id: volume ID + :type volume_id: int + :return: Volume dict if found, None if not found + :rtype: dict + """ + volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id]) + for volume in volume_list.volumes: + if volume.volume_id == volume_id: + if str(volume.delete_time) == "": + return volume + return None + + def get_volume_id(self, vol_name, account_id): + """ + Return volume id from the given (valid) account_id if found + Return None if not found + + :param vol_name: Name of the volume + :type vol_name: str + :param account_id: Account ID + :type account_id: int + + :return: Volume ID of the first matching volume if found. None if not found. + :rtype: int + """ + volume_list = self.elem_connect.list_volumes_for_account(account_id=account_id) + for volume in volume_list.volumes: + if volume.name == vol_name: + # return volume_id + if str(volume.delete_time) == "": + return volume.volume_id + return None + + def volume_id_exists(self, volume_id): + """ + Return volume_id if volume exists for given volume_id + + :param volume_id: volume ID + :type volume_id: int + :return: Volume ID if found, None if not found + :rtype: int + """ + volume_list = self.elem_connect.list_volumes(volume_ids=[volume_id]) + for volume in volume_list.volumes: + if volume.volume_id == volume_id: + if str(volume.delete_time) == "": + return volume.volume_id + return None + + def volume_exists(self, volume, account_id): + """ + Return volume_id if exists, None if not found + + :param volume: Volume ID or Name + :type volume: str + :param account_id: Account ID (valid) + :type account_id: int + :return: Volume ID if found, None if not found + """ + # If volume is an integer, get_by_id + if str(volume).isdigit(): + volume_id = int(volume) + try: + if self.volume_id_exists(volume_id): + return volume_id + except solidfire.common.ApiServerError: + # don't fail, continue and try get_by_name + pass + # get volume by name + volume_id = self.get_volume_id(volume, account_id) + return volume_id + + def get_snapshot(self, snapshot_id, volume_id): + """ + Return snapshot details if found + + :param snapshot_id: Snapshot ID or Name + :type snapshot_id: str + :param volume_id: Account ID (valid) + :type volume_id: int + :return: Snapshot dict if found, None if not found + :rtype: dict + """ + # mandate src_volume_id although not needed by sdk + snapshot_list = self.elem_connect.list_snapshots( + volume_id=volume_id) + for snapshot in snapshot_list.snapshots: + # if actual id is provided + if str(snapshot_id).isdigit() and snapshot.snapshot_id == int(snapshot_id): + return snapshot + # if snapshot name is provided + elif snapshot.name == snapshot_id: + return snapshot + return None + + def account_exists(self, account): + """ + Return account_id if account exists for given account id or name + Raises an exception if account does not exist + + :param account: Account ID or Name + :type account: str + :return: Account ID if found, None if not found + """ + # If account is an integer, get_by_id + if account.isdigit(): + account_id = int(account) + try: + result = self.elem_connect.get_account_by_id(account_id=account_id) + if result.account.account_id == account_id: + return account_id + except solidfire.common.ApiServerError: + # don't fail, continue and try get_by_name + pass + # get account by name, the method returns an Exception if account doesn't exist + result = self.elem_connect.get_account_by_name(username=account) + return result.account.account_id + + def set_element_attributes(self, source): + """ + Return telemetry attributes for the current execution + + :param source: name of the module + :type source: str + :return: a dict containing telemetry attributes + """ + attributes = {} + attributes['config-mgmt'] = 'ansible' + attributes['event-source'] = source + return(attributes) diff --git a/lib/ansible/module_utils/netapp_module.py b/lib/ansible/module_utils/netapp_module.py new file mode 100644 index 0000000000..8e65e6c659 --- /dev/null +++ b/lib/ansible/module_utils/netapp_module.py @@ -0,0 +1,149 @@ +# 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 +# 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 ''' + + +def cmp(a, b): + """ + Python 3 does not have a cmp function, this will do the cmp. + :param a: first object to check + :param b: second object to check + :return: + """ + return (a > b) - (a < b) + + +class NetAppModule(object): + ''' + Common class for NetApp modules + set of support functions to derive actions based + on the current state of the system, and a desired state + ''' + + def __init__(self): + self.log = list() + self.changed = False + self.parameters = {'name': 'not intialized'} + + def set_parameters(self, ansible_params): + self.parameters = dict() + for param in ansible_params: + if ansible_params[param] is not None: + self.parameters[param] = ansible_params[param] + return self.parameters + + def get_cd_action(self, current, desired): + ''' takes a desired state and a current state, and return an action: + create, delete, None + eg: + is_present = 'absent' + some_object = self.get_object(source) + if some_object is not None: + is_present = 'present' + action = cd_action(current=is_present, desired = self.desired.state()) + ''' + if 'state' in desired: + desired_state = desired['state'] + else: + desired_state = 'present' + + if current is None and desired_state == 'absent': + return None + if current is not None and desired_state == 'present': + return None + # change in state + self.changed = True + if current is not None: + return 'delete' + return 'create' + + @staticmethod + def check_keys(current, desired): + ''' TODO: raise an error if keys do not match + with the exception of: + new_name, state in desired + ''' + pass + + def get_modified_attributes(self, current, desired): + ''' takes two lists of attributes and return a list of attributes that are + not in the desired state + It is expected that all attributes of interest are listed in current and + desired. + + NOTE: depending on the attribute, the caller may need to do a modify or a + different operation (eg move volume if the modified attribute is an + aggregate name) + ''' + # if the object does not exist, we can't modify it + modified = dict() + if current is None: + return modified + + # error out if keys do not match + self.check_keys(current, desired) + + # collect changed attributes + for key, value in current.items(): + if key in desired and desired[key] is not None: + if type(value) is list: + value.sort() + desired[key].sort() + if cmp(value, desired[key]) != 0: + modified[key] = desired[key] + if modified: + self.changed = True + return modified + + def is_rename_action(self, source, target): + ''' takes a source and target object, and returns True + if a rename is required + eg: + source = self.get_object(source_name) + target = self.get_object(target_name) + action = is_rename_action(source, target) + :return: None for error, True for rename action, False otherwise + ''' + if source is None and target is None: + # error, do nothing + # cannot rename an non existent resource + # alternatively we could create B + return None + if source is not None and target is not None: + # error, do nothing + # idempotency (or) new_name_is_already_in_use + # alternatively we could delete B and rename A to B + return False + if source is None and target is not None: + # do nothing, maybe the rename was already done + return False + # source is not None and target is None: + # rename is in order + self.changed = True + return True diff --git a/lib/ansible/utils/module_docs_fragments/netapp.py b/lib/ansible/utils/module_docs_fragments/netapp.py index 653d4d1d20..8174f2e5f9 100644 --- a/lib/ansible/utils/module_docs_fragments/netapp.py +++ b/lib/ansible/utils/module_docs_fragments/netapp.py @@ -39,7 +39,7 @@ options: required: true description: - This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required. - For more information, please read the documentation U(https://goo.gl/BRu78Z). + For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/). aliases: ['user'] password: required: true @@ -48,9 +48,20 @@ options: aliases: ['pass'] https: description: - - Enable and disabled https + - Enable and disable https type: bool default: false + validate_certs: + description: + - If set to C(False), the SSL certificates will not be validated. + - This should only set to C(False) used on personally controlled sites using self-signed certificates. + default: true + type: bool + http_port: + description: + - Override the default port (80 or 443) with this port + type: int + requirements: - A physical or virtual clustered Data ONTAP system. The modules were developed with Clustered Data ONTAP 9.3 @@ -74,7 +85,7 @@ options: required: true description: - This can be a Cluster-scoped or SVM-scoped account, depending on whether a Cluster-level or SVM-level API is required. - For more information, please read the documentation U(https://goo.gl/BRu78Z). + For more information, please read the documentation U(https://mysupport.netapp.com/NOW/download/software/nmsdk/9.4/). aliases: ['user'] password: required: true @@ -101,17 +112,21 @@ options: username: required: true description: - - Please ensure that the user has the adequate permissions. For more information, please read the official documentation U(https://goo.gl/ddJa4Q). + - Please ensure that the user has the adequate permissions. For more information, please read the official documentation + U(https://mysupport.netapp.com/documentation/docweb/index.html?productID=62636&language=en-US). + aliases: ['user'] password: required: true description: - Password for the specified user. + aliases: ['pass'] requirements: - - solidfire-sdk-python (1.1.0.92) + - The modules were developed with SolidFire 10.1 + - solidfire-sdk-python (1.1.0.92) or greater. Install using 'pip install solidfire-sdk-python' notes: - - The modules prefixed with C(sf\\_) are built to support the SolidFire storage platform. + - The modules prefixed with na\\_elementsw are built to support the SolidFire storage platform. """ diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index 1a0f01fdea..29fb782817 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -1126,11 +1126,7 @@ lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E323 lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E324 lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E325 lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E326 -lib/ansible/modules/storage/netapp/sf_account_manager.py E322 -lib/ansible/modules/storage/netapp/sf_check_connections.py E322 -lib/ansible/modules/storage/netapp/sf_snapshot_schedule_manager.py E322 lib/ansible/modules/storage/netapp/sf_snapshot_schedule_manager.py E325 -lib/ansible/modules/storage/netapp/sf_volume_access_group_manager.py E322 lib/ansible/modules/storage/netapp/sf_volume_manager.py E322 lib/ansible/modules/storage/netapp/sf_volume_manager.py E325 lib/ansible/modules/storage/purestorage/purefb_fs.py E324