diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
commit | 66cec45960ce1d9c794e9399de15c138acb18aed (patch) | |
tree | 59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/netapp/elementsw/plugins/module_utils | |
parent | Initial commit. (diff) | |
download | ansible-upstream.tar.xz ansible-upstream.zip |
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/netapp/elementsw/plugins/module_utils')
3 files changed, 538 insertions, 0 deletions
diff --git a/ansible_collections/netapp/elementsw/plugins/module_utils/netapp.py b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp.py new file mode 100644 index 00000000..4121bf8e --- /dev/null +++ b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp.py @@ -0,0 +1,107 @@ +# 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) 2018, NetApp Ansible Team <ng-ansibleteam@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. + +''' +Common methods and constants +''' + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +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 +) + +# uncomment this to log API calls +# import logging + +try: + from solidfire.factory import ElementFactory + import solidfire.common + HAS_SF_SDK = True +except ImportError: + HAS_SF_SDK = False + +COLLECTION_VERSION = "21.7.0" + + +def has_sf_sdk(): + return HAS_SF_SDK + + +def ontap_sf_host_argument_spec(): + + return dict( + hostname=dict(required=True, type='str'), + username=dict(required=True, type='str', aliases=['user']), + password=dict(required=True, type='str', aliases=['pass'], no_log=True) + ) + + +def create_sf_connection(module, hostname=None, port=None, raise_on_connection_error=False, timeout=None): + if hostname is None: + hostname = module.params['hostname'] + username = module.params['username'] + password = module.params['password'] + options = dict() + if port is not None: + options['port'] = port + if timeout is not None: + options['timeout'] = timeout + + if not HAS_SF_SDK: + module.fail_json(msg="the python SolidFire SDK module is required") + + try: + logging.basicConfig(filename='/tmp/elementsw_apis.log', level=logging.DEBUG, format='%(asctime)s %(levelname)-8s %(message)s') + except NameError: + # logging was not imported + pass + + try: + return_val = ElementFactory.create(hostname, username, password, **options) + except (solidfire.common.ApiConnectionError, solidfire.common.ApiServerError) as exc: + if raise_on_connection_error: + raise exc + module.fail_json(msg=repr(exc)) + except Exception as exc: + raise Exception("Unable to create SF connection: %s" % repr(exc)) + return return_val diff --git a/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_elementsw_module.py b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_elementsw_module.py new file mode 100644 index 00000000..2d8b92cf --- /dev/null +++ b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_elementsw_module.py @@ -0,0 +1,206 @@ +# This code is part of Ansible, but is an independent component. +# This particular file snippet, and this file snippet only, is BSD licensed. +# Copyright: (c) 2018, NetApp Ansible Team <ng-ansibleteam@netapp.com> + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.module_utils._text import to_native + +HAS_SF_SDK = False +try: + import solidfire.common + HAS_SF_SDK = True +except ImportError: + HAS_SF_SDK = False + + +def has_sf_sdk(): + return HAS_SF_SDK + + +class NaElementSWModule(object): + ''' Support class for common or shared functions ''' + 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 + + @staticmethod + def map_qos_obj_to_dict(qos_obj): + ''' Take a QOS object and return a key, normalize the key names + Interestingly, the APIs are using different ids for create and get + ''' + mappings = [ + ('burst_iops', 'burstIOPS'), + ('min_iops', 'minIOPS'), + ('max_iops', 'maxIOPS'), + ] + qos_dict = vars(qos_obj) + # Align names to create API and module interface + for read, send in mappings: + if read in qos_dict: + qos_dict[send] = qos_dict.pop(read) + return qos_dict + + def get_qos_policy(self, name): + """ + Get QOS Policy + :description: Get QOS Policy object for a given name + :return: object, error + Policy object converted to dict if found, else None + Error text if error, else None + :rtype: dict/None, str/None + """ + try: + qos_policy_list_obj = self.elem_connect.list_qos_policies() + except (solidfire.common.ApiServerError, solidfire.common.ApiConnectionError) as exc: + error = "Error getting list of qos policies: %s" % to_native(exc) + return None, error + + policy_dict = dict() + if hasattr(qos_policy_list_obj, 'qos_policies'): + for policy in qos_policy_list_obj.qos_policies: + # Check and get policy object for a given name + if str(policy.qos_policy_id) == name: + policy_dict = vars(policy) + elif policy.name == name: + policy_dict = vars(policy) + if 'qos' in policy_dict: + policy_dict['qos'] = self.map_qos_obj_to_dict(policy_dict['qos']) + + return policy_dict if policy_dict else None, 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/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_module.py b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_module.py new file mode 100644 index 00000000..c2b02d3d --- /dev/null +++ b/ansible_collections/netapp/elementsw/plugins/module_utils/netapp_module.py @@ -0,0 +1,225 @@ +# 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, NetApp Ansible Team <ng-ansibleteam@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 + + +def cmp(a, b): + """ + Python 3 does not have a cmp function, this will do the cmp. + :param a: first object to check + :param b: second object to check + :return: + """ + # convert to lower case for string comparison. + if a is None: + return -1 + if type(a) is str and type(b) is str: + a = a.lower() + b = b.lower() + # if list has string element, convert string to lower case. + if type(a) is list and type(b) is list: + a = [x.lower() if type(x) is str else x for x in a] + b = [x.lower() if type(x) is str else x for x in b] + a.sort() + b.sort() + return (a > b) - (a < b) + + +class NetAppModule(object): + ''' + Common class for NetApp modules + set of support functions to derive actions based + on the current state of the system, and a desired state + ''' + + def __init__(self): + self.log = list() + self.changed = False + self.parameters = {'name': 'not intialized'} + # self.debug = list() + + 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' + + def compare_and_update_values(self, current, desired, keys_to_compare): + updated_values = dict() + is_changed = False + for key in keys_to_compare: + if key in current: + if key in desired and desired[key] is not None: + if current[key] != desired[key]: + updated_values[key] = desired[key] + is_changed = True + else: + updated_values[key] = current[key] + else: + updated_values[key] = current[key] + + return updated_values, is_changed + + @staticmethod + def check_keys(current, desired): + ''' TODO: raise an error if keys do not match + with the exception of: + new_name, state in desired + ''' + pass + + @staticmethod + def compare_lists(current, desired, get_list_diff): + ''' compares two lists and return a list of elements that are either the desired elements or elements that are + modified from the current state depending on the get_list_diff flag + :param: current: current item attribute in ONTAP + :param: desired: attributes from playbook + :param: get_list_diff: specifies whether to have a diff of desired list w.r.t current list for an attribute + :return: list of attributes to be modified + :rtype: list + ''' + desired_diff_list = [item for item in desired if item not in current] # get what in desired and not in current + current_diff_list = [item for item in current if item not in desired] # get what in current but not in desired + + if desired_diff_list or current_diff_list: + # there are changes + if get_list_diff: + return desired_diff_list + else: + return desired + else: + return [] + + def get_modified_attributes(self, current, desired, get_list_diff=False, additional_keys=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. + The same assumption holds true for any nested directory. + TODO: This is actually not true for the ElementSW 'attributes' directory. + Practically it means you cannot add or remove a key in a modify. + :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) + ''' + # uncomment these 2 lines if needed + # self.log.append('current: %s' % repr(current)) + # self.log.append('desired: %s' % repr(desired)) + # if the object does not exist, we can't modify it + modified = dict() + if current is None: + return modified + + # error out if keys do not match + self.check_keys(current, desired) + + # collect changed attributes + for key, value in current.items(): + if key in desired and desired[key] is not None: + if type(value) is list: + modified_list = self.compare_lists(value, desired[key], get_list_diff) # get modified list from current and desired + if modified_list: + modified[key] = modified_list + elif type(value) is dict: + modified_dict = self.get_modified_attributes(value, desired[key], get_list_diff, additional_keys=True) + if modified_dict: + modified[key] = modified_dict + elif cmp(value, desired[key]) != 0: + modified[key] = desired[key] + if additional_keys: + for key, value in desired.items(): + if key not in current: + modified[key] = desired[key] + if modified: + self.changed = True + # Uncomment this line if needed + # self.log.append('modified: %s' % repr(modified)) + 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 |