summaryrefslogtreecommitdiffstats
path: root/ansible_collections/netapp/elementsw/plugins/module_utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/netapp/elementsw/plugins/module_utils
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/netapp/elementsw/plugins/module_utils/netapp.py107
-rw-r--r--ansible_collections/netapp/elementsw/plugins/module_utils/netapp_elementsw_module.py206
-rw-r--r--ansible_collections/netapp/elementsw/plugins/module_utils/netapp_module.py225
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