diff --git a/lib/ansible/modules/storage/netapp/netapp_e_volume.py b/lib/ansible/modules/storage/netapp/netapp_e_volume.py index 4d3e42c1ea..e92199b158 100644 --- a/lib/ansible/modules/storage/netapp/netapp_e_volume.py +++ b/lib/ansible/modules/storage/netapp/netapp_e_volume.py @@ -11,527 +11,748 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} -DOCUMENTATION = ''' +DOCUMENTATION = """ --- module: netapp_e_volume version_added: "2.2" short_description: NetApp E-Series manage storage volumes (standard and thin) description: - Create or remove volumes (standard and thin) for NetApp E/EF-series storage arrays. +author: + - Kevin Hulquest (@hulquest) + - Nathan Swartz (@ndswartz) extends_documentation_fragment: - netapp.eseries options: - state: - description: - - Whether the specified volume should exist or not. - required: true - choices: ['present', 'absent'] - name: - description: - - The name of the volume to manage - required: true - storage_pool_name: - description: - - "Required only when requested state is 'present'. The name of the storage pool the volume should exist on." - required: true - size_unit: - description: - - The unit used to interpret the size parameter - choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] - default: 'gb' - size: - description: - - "Required only when state = 'present'. The size of the volume in (size_unit)." - required: true - segment_size_kb: - description: - - The segment size of the new volume - default: 512 - thin_provision: - description: - - Whether the volume should be thin provisioned. Thin volumes can only be created on disk pools (raidDiskPool). - type: bool - default: 'no' - thin_volume_repo_size: - description: - - Initial size of the thin volume repository volume (in size_unit) - required: True - thin_volume_max_repo_size: - description: - - Maximum size that the thin volume repository volume will automatically expand to - default: same as size (in size_unit) - ssd_cache_enabled: - description: - - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined) - - The default value is to ignore existing SSD cache setting. - type: bool - data_assurance_enabled: - description: - - If data assurance should be enabled for the volume - type: bool - default: 'no' - -# TODO: doc thin volume parameters - -author: Kevin Hulquest (@hulquest) - -''' -EXAMPLES = ''' - - name: No thin volume - netapp_e_volume: - ssid: "{{ ssid }}" - name: NewThinVolumeByAnsible - state: absent - log_path: /tmp/volume.log - api_url: "{{ netapp_api_url }}" - api_username: "{{ netapp_api_username }}" - api_password: "{{ netapp_api_password }}" - validate_certs: "{{ netapp_api_validate_certs }}" - when: check_volume - - - - name: No fat volume - netapp_e_volume: - ssid: "{{ ssid }}" - name: NewVolumeByAnsible - state: absent - log_path: /tmp/volume.log - api_url: "{{ netapp_api_url }}" - api_username: "{{ netapp_api_username }}" - api_password: "{{ netapp_api_password }}" - validate_certs: "{{ netapp_api_validate_certs }}" - when: check_volume -''' -RETURN = ''' ---- + state: + description: + - Whether the specified volume should exist + required: true + choices: ['present', 'absent'] + name: + description: + - The name of the volume to manage. + required: true + storage_pool_name: + description: + - Required only when requested I(state=='present'). + - Name of the storage pool wherein the volume should reside. + required: false + size_unit: + description: + - The unit used to interpret the size parameter + choices: ['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'] + default: 'gb' + size: + description: + - Required only when I(state=='present'). + - Size of the volume in I(size_unit). + - Size of the virtual volume in the case of a thin volume in I(size_unit). + - Maximum virtual volume size of a thin provisioned volume is 256tb; however other OS-level restrictions may + exist. + required: true + segment_size_kb: + description: + - Segment size of the volume + - All values are in kibibytes. + - Some common choices include '8', '16', '32', '64', '128', '256', and '512' but options are system + dependent. + - Retrieve the definitive system list from M(netapp_e_facts) under segment_sizes. + - When the storage pool is a raidDiskPool then the segment size must be 128kb. + - Segment size migrations are not allowed in this module + default: '128' + thin_provision: + description: + - Whether the volume should be thin provisioned. + - Thin volumes can only be created when I(raid_level=="raidDiskPool"). + - Generally, use of thin-provisioning is not recommended due to performance impacts. + type: bool + default: false + thin_volume_repo_size: + description: + - This value (in size_unit) sets the allocated space for the thin provisioned repository. + - Initial value must between or equal to 4gb and 256gb in increments of 4gb. + - During expansion operations the increase must be between or equal to 4gb and 256gb in increments of 4gb. + - This option has no effect during expansion if I(thin_volume_expansion_policy=="automatic"). + - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic). + required: false + thin_volume_max_repo_size: + description: + - This is the maximum amount the thin volume repository will be allowed to grow. + - Only has significance when I(thin_volume_expansion_policy=="automatic"). + - When the percentage I(thin_volume_repo_size) of I(thin_volume_max_repo_size) exceeds + I(thin_volume_growth_alert_threshold) then a warning will be issued and the storage array will execute + the I(thin_volume_expansion_policy) policy. + - Expansion operations when I(thin_volume_expansion_policy=="automatic") will increase the maximum + repository size. + default: same as size (in size_unit) + thin_volume_expansion_policy: + description: + - This is the thin volume expansion policy. + - When I(thin_volume_expansion_policy=="automatic") and I(thin_volume_growth_alert_threshold) is exceed the + I(thin_volume_max_repo_size) will be automatically expanded. + - When I(thin_volume_expansion_policy=="manual") and I(thin_volume_growth_alert_threshold) is exceeded the + storage system will wait for manual intervention. + - The thin volume_expansion policy can not be modified on existing thin volumes in this module. + - Generally speaking you should almost always use I(thin_volume_expansion_policy=="automatic). + choices: ["automatic", "manual"] + default: "automatic" + version_added: 2.8 + thin_volume_growth_alert_threshold: + description: + - This is the thin provision repository utilization threshold (in percent). + - When the percentage of used storage of the maximum repository size exceeds this value then a alert will + be issued and the I(thin_volume_expansion_policy) will be executed. + - Values must be between or equal to 10 and 99. + default: 95 + version_added: 2.8 + ssd_cache_enabled: + description: + - Whether an existing SSD cache should be enabled on the volume (fails if no SSD cache defined) + - The default value is to ignore existing SSD cache setting. + type: bool + default: false + data_assurance_enabled: + description: + - Determines whether data assurance (DA) should be enabled for the volume + - Only available when creating a new volume and on a storage pool with drives supporting the DA capability. + type: bool + default: false + read_cache_enable: + description: + - Indicates whether read caching should be enabled for the volume. + type: bool + default: true + version_added: 2.8 + read_ahead_enable: + description: + - Indicates whether or not automatic cache read-ahead is enabled. + - This option has no effect on thinly provisioned volumes since the architecture for thin volumes cannot + benefit from read ahead caching. + type: bool + default: false + version_added: 2.8 + write_cache_enable: + description: + - Indicates whether write-back caching should be enabled for the volume. + type: bool + default: false + version_added: 2.8 + workload_name: + description: + - Label for the workload defined by the metadata. + - When I(workload_name) and I(metadata) are specified then the defined workload will be added to the storage + array. + - When I(workload_name) exists on the storage array but the metadata is different then the workload + definition will be updated. (Changes will update all associated volumes!) + - Existing workloads can be retrieved using M(netapp_e_facts). + required: false + version_added: 2.8 + metadata: + description: + - Dictionary containing meta data for the use, user, location, etc of the volume (dictionary is arbitrarily + defined for whatever the user deems useful) + - When I(workload_name) exists on the storage array but the metadata is different then the workload + definition will be updated. (Changes will update all associated volumes!) + - I(workload_name) must be specified when I(metadata) are defined. + type: dict + required: false + version_added: 2.8 +""" +EXAMPLES = """ +- name: Create simple volume with workload tags (volume meta data) + netapp_e_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: present + name: volume + storage_pool_name: storage_pool + size: 300 + size_unit: gb + workload_name: volume_tag + metadata: + key1: value1 + key2: value2 +- name: Create a thin volume + netapp_e_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: present + name: volume1 + storage_pool_name: storage_pool + size: 131072 + size_unit: gb + thin_provision: true + thin_volume_repo_size: 32 + thin_volume_max_repo_size: 1024 +- name: Expand thin volume's virtual size + netapp_e_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: present + name: volume1 + storage_pool_name: storage_pool + size: 262144 + size_unit: gb + thin_provision: true + thin_volume_repo_size: 32 + thin_volume_max_repo_size: 1024 +- name: Expand thin volume's maximum repository size + netapp_e_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: present + name: volume1 + storage_pool_name: storage_pool + size: 262144 + size_unit: gb + thin_provision: true + thin_volume_repo_size: 32 + thin_volume_max_repo_size: 2048 +- name: Delete volume + netapp_e_volume: + ssid: "{{ ssid }}" + api_url: "{{ netapp_api_url }}" + api_username: "{{ netapp_api_username }}" + api_password: "{{ netapp_api_password }}" + validate_certs: "{{ netapp_api_validate_certs }}" + state: absent + name: volume +""" +RETURN = """ msg: description: State of volume type: str returned: always sample: "Standard volume [workload_vol_1] has been created." -''' +""" -import json -import logging import time -from traceback import format_exc -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.netapp import request, eseries_host_argument_spec +from ansible.module_utils.netapp import NetAppESeriesModule from ansible.module_utils._text import to_native -HEADERS = { - "Content-Type": "application/json", - "Accept": "application/json", -} +class NetAppESeriesVolume(NetAppESeriesModule): + VOLUME_CREATION_BLOCKING_TIMEOUT_SEC = 300 -def ifilter(predicate, iterable): - # python 2, 3 generic filtering. - if predicate is None: - predicate = bool - for x in iterable: - if predicate(x): - yield x - - -class NetAppESeriesVolume(object): def __init__(self): - self._size_unit_map = dict( - bytes=1, - b=1, - kb=1024, - mb=1024 ** 2, - gb=1024 ** 3, - tb=1024 ** 4, - pb=1024 ** 5, - eb=1024 ** 6, - zb=1024 ** 7, - yb=1024 ** 8 - ) + ansible_options = dict( + state=dict(required=True, choices=["present", "absent"]), + name=dict(required=True, type="str"), + storage_pool_name=dict(type="str"), + size_unit=dict(default="gb", choices=["bytes", "b", "kb", "mb", "gb", "tb", "pb", "eb", "zb", "yb"], + type="str"), + size=dict(type="float"), + segment_size_kb=dict(type="int", default=128), + ssd_cache_enabled=dict(type="bool", default=False), + data_assurance_enabled=dict(type="bool", default=False), + thin_provision=dict(type="bool", default=False), + thin_volume_repo_size=dict(type="int"), + thin_volume_max_repo_size=dict(type="float"), + thin_volume_expansion_policy=dict(type="str", choices=["automatic", "manual"]), + thin_volume_growth_alert_threshold=dict(type="int", default=95), + read_cache_enable=dict(type="bool", default=True), + read_ahead_enable=dict(type="bool", default=False), + write_cache_enable=dict(type="bool", default=False), + workload_name=dict(type="str", required=False), + metadata=dict(type="dict", require=False)) - argument_spec = eseries_host_argument_spec() - argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent']), - name=dict(required=True, type='str'), - storage_pool_name=dict(type='str'), - size_unit=dict(default='gb', choices=['bytes', 'b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'], - type='str'), - size=dict(type='int'), - segment_size_kb=dict(default=128, choices=[8, 16, 32, 64, 128, 256, 512], type='int'), - ssd_cache_enabled=dict(type='bool'), # no default, leave existing setting alone - data_assurance_enabled=dict(default=False, type='bool'), - thin_provision=dict(default=False, type='bool'), - thin_volume_repo_size=dict(type='int'), - thin_volume_max_repo_size=dict(type='int'), - # TODO: add cache, owning controller support, thin expansion policy, etc - log_path=dict(type='str'), - )) + required_if = [ + ["state", "present", ["storage_pool_name", "size"]], + ["thin_provision", "true", ["thin_volume_repo_size"]] + ] - self.module = AnsibleModule(argument_spec=argument_spec, - required_if=[ - ('state', 'present', ['storage_pool_name', 'size']), - ('thin_provision', 'true', ['thin_volume_repo_size']) - ], - supports_check_mode=True) - p = self.module.params + super(NetAppESeriesVolume, self).__init__(ansible_options=ansible_options, + web_services_version="02.00.0000.0000", + supports_check_mode=True, + required_if=required_if) - log_path = p['log_path'] + args = self.module.params + self.state = args["state"] + self.name = args["name"] + self.storage_pool_name = args["storage_pool_name"] + self.size_unit = args["size_unit"] + self.segment_size_kb = args["segment_size_kb"] + if args["size"]: + self.size_b = int(args["size"] * self.SIZE_UNIT_MAP[self.size_unit]) - # logging setup - self._logger = logging.getLogger(self.__class__.__name__) - self.debug = self._logger.debug + self.read_cache_enable = args["read_cache_enable"] + self.read_ahead_enable = args["read_ahead_enable"] + self.write_cache_enable = args["write_cache_enable"] + self.ssd_cache_enabled = args["ssd_cache_enabled"] + self.data_assurance_enabled = args["data_assurance_enabled"] - if log_path: - logging.basicConfig(level=logging.DEBUG, filename=log_path) + self.thin_provision = args["thin_provision"] + self.thin_volume_expansion_policy = args["thin_volume_expansion_policy"] + self.thin_volume_growth_alert_threshold = int(args["thin_volume_growth_alert_threshold"]) + self.thin_volume_repo_size_b = None + self.thin_volume_max_repo_size_b = None + if args["thin_volume_repo_size"]: + self.thin_volume_repo_size_b = args["thin_volume_repo_size"] * self.SIZE_UNIT_MAP[self.size_unit] + if args["thin_volume_max_repo_size"]: + self.thin_volume_max_repo_size_b = int(args["thin_volume_max_repo_size"] * + self.SIZE_UNIT_MAP[self.size_unit]) - self.state = p['state'] - self.ssid = p['ssid'] - self.name = p['name'] - self.storage_pool_name = p['storage_pool_name'] - self.size_unit = p['size_unit'] - self.size = p['size'] - self.segment_size_kb = p['segment_size_kb'] - self.ssd_cache_enabled = p['ssd_cache_enabled'] - self.data_assurance_enabled = p['data_assurance_enabled'] - self.thin_provision = p['thin_provision'] - self.thin_volume_repo_size = p['thin_volume_repo_size'] - self.thin_volume_max_repo_size = p['thin_volume_max_repo_size'] + self.workload_name = args["workload_name"] + self.metadata = args["metadata"] - if not self.thin_volume_max_repo_size: - self.thin_volume_max_repo_size = self.size + # convert metadata to a list of dictionaries containing the keys "key" and "value" corresponding to + # each of the workload attributes dictionary entries + metadata = [] + if self.metadata: + if not self.workload_name: + self.module.fail_json(msg="When metadata is specified then the name for the workload must be specified." + " Array [%s]." % self.ssid) + for key in self.metadata.keys(): + metadata.append(dict(key=key, value=self.metadata[key])) + self.metadata = metadata - self.validate_certs = p['validate_certs'] + if self.thin_provision: + if not self.thin_volume_max_repo_size_b: + self.thin_volume_max_repo_size_b = self.size_b + if not self.thin_volume_expansion_policy: + self.thin_volume_expansion_policy = "automatic" + + if self.size_b > 256 * 1024 ** 4: + self.module.fail_json(msg="Thin provisioned volumes must be less than or equal to 256tb is size." + " Attempted size [%sg]" % (self.size_b * 1024 ** 3)) + + if (self.thin_volume_repo_size_b and self.thin_volume_max_repo_size_b and + self.thin_volume_repo_size_b > self.thin_volume_max_repo_size_b): + self.module.fail_json(msg="The initial size of the thin volume must not be larger than the maximum" + " repository size. Array [%s]." % self.ssid) + + if self.thin_volume_growth_alert_threshold < 10 or self.thin_volume_growth_alert_threshold > 99: + self.module.fail_json(msg="thin_volume_growth_alert_threshold must be between or equal to 10 and 99." + "thin_volume_growth_alert_threshold [%s]. Array [%s]." + % (self.thin_volume_growth_alert_threshold, self.ssid)) + + self.volume_detail = None + self.pool_detail = None + self.workload_id = None + + def get_volume(self): + """Retrieve volume details from storage array.""" + volumes = list() + thin_volumes = list() try: - self.api_usr = p['api_username'] - self.api_pwd = p['api_password'] - self.api_url = p['api_url'] - except KeyError: - self.module.fail_json(msg="You must pass in api_username " - "and api_password and api_url to the module.") - - def get_volume(self, volume_name): - self.debug('fetching volumes') - # fetch the list of volume objects and look for one with a matching name (we'll need to merge volumes and thin-volumes) - try: - (rc, volumes) = request(self.api_url + "/storage-systems/%s/volumes" % (self.ssid), - headers=dict(Accept="application/json"), url_username=self.api_usr, - url_password=self.api_pwd, validate_certs=self.validate_certs) + rc, volumes = self.request("storage-systems/%s/volumes" % self.ssid) except Exception as err: - self.module.fail_json( - msg="Failed to obtain list of standard/thick volumes. Array Id [%s]. Error[%s]." % (self.ssid, - to_native(err))) - + self.module.fail_json(msg="Failed to obtain list of thick volumes. Array Id [%s]. Error[%s]." + % (self.ssid, to_native(err))) try: - self.debug('fetching thin-volumes') - (rc, thinvols) = request(self.api_url + "/storage-systems/%s/thin-volumes" % (self.ssid), - headers=dict(Accept="application/json"), url_username=self.api_usr, - url_password=self.api_pwd, validate_certs=self.validate_certs) + rc, thin_volumes = self.request("storage-systems/%s/thin-volumes" % self.ssid) except Exception as err: - self.module.fail_json( - msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]." % (self.ssid, to_native(err))) + self.module.fail_json(msg="Failed to obtain list of thin volumes. Array Id [%s]. Error[%s]." + % (self.ssid, to_native(err))) - volumes.extend(thinvols) + volume_detail = [volume for volume in volumes + thin_volumes if volume["name"] == self.name] + return volume_detail[0] if volume_detail else dict() - self.debug("searching for volume '%s'", volume_name) - volume_detail = next(ifilter(lambda a: a['name'] == volume_name, volumes), None) + def wait_for_volume_availability(self, retries=VOLUME_CREATION_BLOCKING_TIMEOUT_SEC / 5): + """Waits until volume becomes available. - if volume_detail: - self.debug('found') + :raises AnsibleFailJson when retries are exhausted. + """ + if retries == 0: + self.module.fail_json(msg="Timed out waiting for the volume %s to become available. Array [%s]." + % (self.name, self.ssid)) + if not self.get_volume(): + time.sleep(5) + self.wait_for_volume_availability(retries=retries - 1) + + def get_storage_pool(self): + """Retrieve storage pool details from the storage array.""" + storage_pools = list() + try: + rc, storage_pools = self.request("storage-systems/%s/storage-pools" % self.ssid) + except Exception as err: + self.module.fail_json(msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]." + % (self.ssid, to_native(err))) + + pool_detail = [storage_pool for storage_pool in storage_pools if storage_pool["name"] == self.storage_pool_name] + return pool_detail[0] if pool_detail else dict() + + def check_storage_pool_sufficiency(self): + """Perform a series of checks as to the sufficiency of the storage pool for the volume.""" + if not self.pool_detail: + self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name) + + if not self.volume_detail: + if self.thin_provision and not self.pool_detail['diskPool']: + self.module.fail_json(msg='Thin provisioned volumes can only be created on raid disk pools.') + + if (self.data_assurance_enabled and not + (self.pool_detail["protectionInformationCapabilities"]["protectionInformationCapable"] and + self.pool_detail["protectionInformationCapabilities"]["protectionType"] == "type2Protection")): + self.module.fail_json(msg="Data Assurance (DA) requires the storage pool to be DA-compatible." + " Array [%s]." % self.ssid) + + if int(self.pool_detail["freeSpace"]) < self.size_b and not self.thin_provision: + self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs." + " Array [%s]." % self.ssid) else: - self.debug('not found') + # Check for expansion + if (int(self.pool_detail["freeSpace"]) < int(self.volume_detail["totalSizeInBytes"]) - self.size_b and + not self.thin_provision): + self.module.fail_json(msg="Not enough storage pool free space available for the volume's needs." + " Array [%s]." % self.ssid) - return volume_detail + def update_workload_tags(self, check_mode=False): + """Check the status of the workload tag and update storage array definitions if necessary. - def get_storage_pool(self, storage_pool_name): - self.debug("fetching storage pools") - # map the storage pool name to its id - try: - (rc, resp) = request(self.api_url + "/storage-systems/%s/storage-pools" % (self.ssid), - headers=dict(Accept="application/json"), url_username=self.api_usr, - url_password=self.api_pwd, validate_certs=self.validate_certs) - except Exception as err: - self.module.fail_json( - msg="Failed to obtain list of storage pools. Array Id [%s]. Error[%s]." % (self.ssid, to_native(err))) + When the workload attributes are not provided but an existing workload tag name is, then the attributes will be + used. - self.debug("searching for storage pool '%s'", storage_pool_name) - pool_detail = next(ifilter(lambda a: a['name'] == storage_pool_name, resp), None) + :return bool: Whether changes were required to be made.""" + change_required = False + workload_tags = None + request_body = None + ansible_profile_id = None + + if self.workload_name: + try: + rc, workload_tags = self.request("storage-systems/%s/workloads" % self.ssid) + except Exception as error: + self.module.fail_json(msg="Failed to retrieve storage array workload tags. Array [%s]" % self.ssid) + + # Generate common indexed Ansible workload tag + tag_index = max([int(pair["value"].replace("ansible_workload_", "")) + for tag in workload_tags for pair in tag["workloadAttributes"] + if pair["key"] == "profileId" and "ansible_workload_" in pair["value"] and + str(pair["value"]).replace("ansible_workload_", "").isdigit()]) + 1 + + ansible_profile_id = "ansible_workload_%d" % tag_index + request_body = dict(name=self.workload_name, + profileId=ansible_profile_id, + workloadInstanceIndex=None, + isValid=True) + + # evaluate and update storage array when needed + for tag in workload_tags: + if tag["name"] == self.workload_name: + self.workload_id = tag["id"] + + if not self.metadata: + break + + # Determine if core attributes (everything but profileId) is the same + metadata_set = set(tuple(sorted(attr.items())) for attr in self.metadata) + tag_set = set(tuple(sorted(attr.items())) + for attr in tag["workloadAttributes"] if attr["key"] != "profileId") + if metadata_set != tag_set: + self.module.log("Workload tag change is required!") + change_required = True + + # only perform the required action when check_mode==False + if change_required and not check_mode: + self.metadata.append(dict(key="profileId", value=ansible_profile_id)) + request_body.update(dict(isNewWorkloadInstance=False, + isWorkloadDataInitialized=True, + isWorkloadCardDataToBeReset=True, + workloadAttributes=self.metadata)) + try: + rc, resp = self.request("storage-systems/%s/workloads/%s" % (self.ssid, tag["id"]), + data=request_body, method="POST") + except Exception as error: + self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]" + % (self.ssid, to_native(error))) + self.module.log("Workload tag [%s] required change." % self.workload_name) + break + + # existing workload tag not found so create new workload tag + else: + change_required = True + self.module.log("Workload tag creation is required!") + + if change_required and not check_mode: + if self.metadata: + self.metadata.append(dict(key="profileId", value=ansible_profile_id)) + else: + self.metadata = [dict(key="profileId", value=ansible_profile_id)] + + request_body.update(dict(isNewWorkloadInstance=True, + isWorkloadDataInitialized=False, + isWorkloadCardDataToBeReset=False, + workloadAttributes=self.metadata)) + try: + rc, resp = self.request("storage-systems/%s/workloads" % self.ssid, + method="POST", data=request_body) + self.workload_id = resp["id"] + except Exception as error: + self.module.fail_json(msg="Failed to create new workload tag. Array [%s]. Error [%s]" + % (self.ssid, to_native(error))) + self.module.log("Workload tag [%s] was added." % self.workload_name) + + return change_required + + def get_volume_property_changes(self): + """Retrieve the volume update request body when change(s) are required. + + :raise AnsibleFailJson when attempting to change segment size on existing volume. + :return dict: request body when change(s) to a volume's properties are required. + """ + change = False + request_body = dict(flashCache=self.ssd_cache_enabled, metaTags=[], + cacheSettings=dict(readCacheEnable=self.read_cache_enable, + writeCacheEnable=self.write_cache_enable)) + + # check for invalid modifications + if self.segment_size_kb * 1024 != int(self.volume_detail["segmentSize"]): + self.module.fail_json(msg="Existing volume segment size is %s and cannot be modified." + % self.volume_detail["segmentSize"]) + + # common thick/thin volume properties + if (self.read_cache_enable != self.volume_detail["cacheSettings"]["readCacheEnable"] or + self.write_cache_enable != self.volume_detail["cacheSettings"]["writeCacheEnable"] or + self.ssd_cache_enabled != self.volume_detail["flashCached"]): + change = True + + if self.workload_name: + request_body.update(dict(metaTags=[dict(key="workloadId", value=self.workload_id), + dict(key="volumeTypeId", value="volume")])) + if {"key": "workloadId", "value": self.workload_id} not in self.volume_detail["metadata"]: + change = True + elif self.volume_detail["metadata"]: + change = True + + # thick/thin volume specific properties + if self.thin_provision: + if self.thin_volume_growth_alert_threshold != int(self.volume_detail["growthAlertThreshold"]): + change = True + request_body.update(dict(growthAlertThreshold=self.thin_volume_growth_alert_threshold)) + if self.thin_volume_expansion_policy != self.volume_detail["expansionPolicy"]: + change = True + request_body.update(dict(expansionPolicy=self.thin_volume_expansion_policy)) + elif self.read_ahead_enable != (int(self.volume_detail["cacheSettings"]["readAheadMultiplier"]) > 0): + change = True + request_body["cacheSettings"].update(dict(readAheadEnable=self.read_ahead_enable)) + + return request_body if change else dict() + + def get_expand_volume_changes(self): + """Expand the storage specifications for the existing thick/thin volume. + + :raise AnsibleFailJson when a thick/thin volume expansion request fails. + :return dict: dictionary containing all the necessary values for volume expansion request + """ + request_body = dict() + + if self.size_b < int(self.volume_detail["capacity"]): + self.module.fail_json(msg="Reducing the size of volumes is not permitted. Volume [%s]. Array [%s]" + % (self.name, self.ssid)) + + if self.volume_detail["thinProvisioned"]: + if self.size_b > int(self.volume_detail["capacity"]): + request_body.update(dict(sizeUnit="bytes", newVirtualSize=self.size_b)) + self.module.log("Thin volume virtual size have been expanded.") + + if self.volume_detail["expansionPolicy"] == "automatic": + if self.thin_volume_max_repo_size_b > int(self.volume_detail["provisionedCapacityQuota"]): + request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_max_repo_size_b)) + self.module.log("Thin volume maximum repository size have been expanded (automatic policy).") + + elif self.volume_detail["expansionPolicy"] == "manual": + if self.thin_volume_repo_size_b > int(self.volume_detail["currentProvisionedCapacity"]): + change = self.thin_volume_repo_size_b - int(self.volume_detail["currentProvisionedCapacity"]) + if change < 4 * 1024 ** 3 or change > 256 * 1024 ** 3 or change % (4 * 1024 ** 3) != 0: + self.module.fail_json(msg="The thin volume repository increase must be between or equal to 4gb" + " and 256gb in increments of 4gb. Attempted size [%sg]." + % (self.thin_volume_repo_size_b * 1024 ** 3)) + + request_body.update(dict(sizeUnit="bytes", newRepositorySize=self.thin_volume_repo_size_b)) + self.module.log("Thin volume maximum repository size have been expanded (manual policy).") + + elif self.size_b > int(self.volume_detail["capacity"]): + request_body.update(dict(sizeUnit="bytes", expansionSize=self.size_b)) + self.module.log("Volume storage capacities have been expanded.") + + return request_body + + def create_volume(self): + """Create thick/thin volume according to the specified criteria.""" + body = dict(name=self.name, poolId=self.pool_detail["id"], sizeUnit="bytes", + dataAssuranceEnabled=self.data_assurance_enabled) + + if self.thin_provision: + body.update(dict(virtualSize=self.size_b, + repositorySize=self.thin_volume_repo_size_b, + maximumRepositorySize=self.thin_volume_max_repo_size_b, + expansionPolicy=self.thin_volume_expansion_policy, + growthAlertThreshold=self.thin_volume_growth_alert_threshold)) + try: + rc, volume = self.request("storage-systems/%s/thin-volumes" % self.ssid, data=body, method="POST") + except Exception as error: + self.module.fail_json(msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(error))) + + self.module.log("New thin volume created [%s]." % self.name) - if pool_detail: - self.debug('found') else: - self.debug('not found') + body.update(dict(size=self.size_b, segSize=self.segment_size_kb)) + try: + rc, volume = self.request("storage-systems/%s/volumes" % self.ssid, data=body, method="POST") + except Exception as error: + self.module.fail_json(msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(error))) - return pool_detail - - def create_volume(self, pool_id, name, size_unit, size, segment_size_kb, data_assurance_enabled): - volume_add_req = dict( - name=name, - poolId=pool_id, - sizeUnit=size_unit, - size=size, - segSize=segment_size_kb, - dataAssuranceEnabled=data_assurance_enabled, - ) - - self.debug("creating volume '%s'", name) - try: - (rc, resp) = request(self.api_url + "/storage-systems/%s/volumes" % (self.ssid), - data=json.dumps(volume_add_req), headers=HEADERS, method='POST', - url_username=self.api_usr, url_password=self.api_pwd, - validate_certs=self.validate_certs, - timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to create volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, self.ssid, - to_native(err))) - - def create_thin_volume(self, pool_id, name, size_unit, size, thin_volume_repo_size, - thin_volume_max_repo_size, data_assurance_enabled): - thin_volume_add_req = dict( - name=name, - poolId=pool_id, - sizeUnit=size_unit, - virtualSize=size, - repositorySize=thin_volume_repo_size, - maximumRepositorySize=thin_volume_max_repo_size, - dataAssuranceEnabled=data_assurance_enabled, - ) - - self.debug("creating thin-volume '%s'", name) - try: - (rc, resp) = request(self.api_url + "/storage-systems/%s/thin-volumes" % (self.ssid), - data=json.dumps(thin_volume_add_req), headers=HEADERS, method='POST', - url_username=self.api_usr, url_password=self.api_pwd, - validate_certs=self.validate_certs, - timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to create thin volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, - self.ssid, - to_native(err))) - - def delete_volume(self): - # delete the volume - self.debug("deleting volume '%s'", self.volume_detail['name']) - try: - (rc, resp) = request( - self.api_url + "/storage-systems/%s/%s/%s" % (self.ssid, self.volume_resource_name, - self.volume_detail['id']), - method='DELETE', url_username=self.api_usr, url_password=self.api_pwd, - validate_certs=self.validate_certs, timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, self.ssid, - to_native(err))) - - @property - def volume_resource_name(self): - if self.volume_detail['thinProvisioned']: - return 'thin-volumes' - else: - return 'volumes' - - @property - def volume_properties_changed(self): - return self.volume_ssdcache_setting_changed # or with other props here when extended - - # TODO: add support for r/w cache settings, owning controller, scan settings, expansion policy, growth alert threshold - - @property - def volume_ssdcache_setting_changed(self): - # None means ignore existing setting - if self.ssd_cache_enabled is not None and self.ssd_cache_enabled != self.volume_detail['flashCached']: - self.debug("flash cache setting changed") - return True + self.module.log("New volume created [%s]." % self.name) def update_volume_properties(self): - update_volume_req = dict() + """Update existing thin-volume or volume properties. - # conditionally add values so we ignore unspecified props - if self.volume_ssdcache_setting_changed: - update_volume_req['flashCache'] = self.ssd_cache_enabled + :raise AnsibleFailJson when either thick/thin volume update request fails. + :return bool: whether update was applied + """ + self.wait_for_volume_availability() + self.volume_detail = self.get_volume() - self.debug("updating volume properties...") - try: - (rc, resp) = request( - self.api_url + "/storage-systems/%s/%s/%s/" % (self.ssid, self.volume_resource_name, - self.volume_detail['id']), - data=json.dumps(update_volume_req), headers=HEADERS, method='POST', - url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, - timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to update volume properties. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, - self.ssid, - to_native(err))) + request_body = self.get_volume_property_changes() - @property - def volume_needs_expansion(self): - current_size_bytes = int(self.volume_detail['capacity']) - requested_size_bytes = self.size * self._size_unit_map[self.size_unit] - - # TODO: check requested/current repo volume size for thin-volumes as well - - # TODO: do we need to build any kind of slop factor in here? - return requested_size_bytes > current_size_bytes + if request_body: + if self.thin_provision: + try: + rc, resp = self.request("storage-systems/%s/thin-volumes/%s" + % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST") + except Exception as error: + self.module.fail_json(msg="Failed to update thin volume properties. Volume [%s]. Array Id [%s]." + " Error[%s]." % (self.name, self.ssid, to_native(error))) + else: + try: + rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]), + data=request_body, method="POST") + except Exception as error: + self.module.fail_json(msg="Failed to update volume properties. Volume [%s]. Array Id [%s]." + " Error[%s]." % (self.name, self.ssid, to_native(error))) + return True + return False def expand_volume(self): - is_thin = self.volume_detail['thinProvisioned'] - if is_thin: - # TODO: support manual repo expansion as well - self.debug('expanding thin volume') - thin_volume_expand_req = dict( - newVirtualSize=self.size, - sizeUnit=self.size_unit - ) - try: - (rc, resp) = request(self.api_url + "/storage-systems/%s/thin-volumes/%s/expand" % (self.ssid, - self.volume_detail[ - 'id']), - data=json.dumps(thin_volume_expand_req), headers=HEADERS, method='POST', - url_username=self.api_usr, url_password=self.api_pwd, - validate_certs=self.validate_certs, timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, - self.ssid, - to_native(err))) + """Expand the storage specifications for the existing thick/thin volume. - # TODO: check return code - else: - self.debug('expanding volume') - volume_expand_req = dict( - expansionSize=self.size, - sizeUnit=self.size_unit - ) - try: - (rc, resp) = request( - self.api_url + "/storage-systems/%s/volumes/%s/expand" % (self.ssid, - self.volume_detail['id']), - data=json.dumps(volume_expand_req), headers=HEADERS, method='POST', - url_username=self.api_usr, url_password=self.api_pwd, validate_certs=self.validate_certs, - timeout=120) - except Exception as err: - self.module.fail_json( - msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]." % (self.name, - self.ssid, - to_native(err))) - - self.debug('polling for completion...') - - while True: + :raise AnsibleFailJson when a thick/thin volume expansion request fails. + """ + request_body = self.get_expand_volume_changes() + if request_body: + if self.volume_detail["thinProvisioned"]: try: - (rc, resp) = request(self.api_url + "/storage-systems/%s/volumes/%s/expand" % (self.ssid, - self.volume_detail[ - 'id']), - method='GET', url_username=self.api_usr, url_password=self.api_pwd, - validate_certs=self.validate_certs) + rc, resp = self.request("storage-systems/%s/thin-volumes/%s/expand" + % (self.ssid, self.volume_detail["id"]), data=request_body, method="POST") except Exception as err: - self.module.fail_json( - msg="Failed to get volume expansion progress. Volume [%s]. Array Id [%s]. Error[%s]." % ( - self.name, self.ssid, to_native(err))) + self.module.fail_json(msg="Failed to expand thin volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(err))) + self.module.log("Thin volume specifications have been expanded.") - action = resp['action'] - percent_complete = resp['percentComplete'] + else: + try: + rc, resp = self.request( + "storage-systems/%s/volumes/%s/expand" % (self.ssid, self.volume_detail['id']), + data=request_body, method="POST") + except Exception as err: + self.module.fail_json(msg="Failed to expand volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(err))) - self.debug('expand action %s, %s complete...', action, percent_complete) + self.module.log("Volume storage capacities have been expanded.") - if action == 'none': - self.debug('expand complete') - break - else: - time.sleep(5) + def delete_volume(self): + """Delete existing thin/thick volume.""" + if self.thin_provision: + try: + rc, resp = self.request("storage-systems/%s/thin-volumes/%s" % (self.ssid, self.volume_detail["id"]), + method="DELETE") + except Exception as error: + self.module.fail_json(msg="Failed to delete thin volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(error))) + self.module.log("Thin volume deleted [%s]." % self.name) + else: + try: + rc, resp = self.request("storage-systems/%s/volumes/%s" % (self.ssid, self.volume_detail["id"]), + method="DELETE") + except Exception as error: + self.module.fail_json(msg="Failed to delete volume. Volume [%s]. Array Id [%s]. Error[%s]." + % (self.name, self.ssid, to_native(error))) + self.module.log("Volume deleted [%s]." % self.name) def apply(self): - changed = False - volume_exists = False + """Determine and apply any changes necessary to satisfy the specified criteria. + + :raise AnsibleExitJson when completes successfully""" + change = False msg = None - self.volume_detail = self.get_volume(self.name) + self.volume_detail = self.get_volume() + self.pool_detail = self.get_storage_pool() + # Determine whether changes need to be applied to existing workload tags + if self.state == 'present' and self.update_workload_tags(check_mode=True): + change = True + + # Determine if any changes need to be applied if self.volume_detail: - volume_exists = True - if self.state == 'absent': - self.debug("CHANGED: volume exists, but requested state is 'absent'") - changed = True + change = True + elif self.state == 'present': - # check requested volume size, see if expansion is necessary - if self.volume_needs_expansion: - self.debug("CHANGED: requested volume size %s%s is larger than current size %sb", - self.size, self.size_unit, self.volume_detail['capacity']) - changed = True + if self.get_expand_volume_changes() or self.get_volume_property_changes(): + change = True - if self.volume_properties_changed: - self.debug("CHANGED: one or more volume properties have changed") - changed = True + elif self.state == 'present': + if self.thin_provision and (self.thin_volume_repo_size_b < 4 * 1024 ** 3 or + self.thin_volume_repo_size_b > 256 * 1024 ** 3 or + self.thin_volume_repo_size_b % (4 * 1024 ** 3) != 0): + self.module.fail_json(msg="The initial thin volume repository size must be between 4gb and 256gb in" + " increments of 4gb. Attempted size [%sg]." + % (self.thin_volume_repo_size_b * 1024 ** 3)) + change = True - else: + self.module.log("Update required: [%s]." % change) + + # Apply any necessary changes + if change and not self.module.check_mode: if self.state == 'present': - self.debug("CHANGED: volume does not exist, but requested state is 'present'") - changed = True + if self.update_workload_tags(): + msg = "Workload tag change occurred." - if changed: - if self.module.check_mode: - self.debug('skipping changes due to check mode') - else: - if self.state == 'present': - if not volume_exists: - pool_detail = self.get_storage_pool(self.storage_pool_name) + if not self.volume_detail: + self.check_storage_pool_sufficiency() + self.create_volume() + self.update_volume_properties() + msg = msg[:-1] + " and volume [%s] was created." if msg else "Volume [%s] has been created." + else: + if self.update_volume_properties(): + msg = "Volume [%s] properties were updated." - if not pool_detail: - self.module.fail_json(msg='Requested storage pool (%s) not found' % self.storage_pool_name) + if self.get_expand_volume_changes(): + self.expand_volume() + msg = msg[:-1] + " and was expanded." if msg else "Volume [%s] was expanded." - if self.thin_provision and not pool_detail['diskPool']: - self.module.fail_json( - msg='Thin provisioned volumes can only be located on disk pools (not volume groups)') + elif self.state == 'absent': + self.delete_volume() + msg = "Volume [%s] has been deleted." - pool_id = pool_detail['id'] - - if not self.thin_provision: - self.create_volume(pool_id, self.name, self.size_unit, self.size, self.segment_size_kb, - self.data_assurance_enabled) - msg = "Standard volume [%s] has been created." % (self.name) - - else: - self.create_thin_volume(pool_id, self.name, self.size_unit, self.size, - self.thin_volume_repo_size, self.thin_volume_max_repo_size, - self.data_assurance_enabled) - msg = "Thin volume [%s] has been created." % (self.name) - - else: # volume exists but differs, modify... - if self.volume_needs_expansion: - self.expand_volume() - msg = "Volume [%s] has been expanded." % (self.name) - - # this stuff always needs to run on present (since props can't be set on creation) - if self.volume_properties_changed: - self.update_volume_properties() - msg = "Properties of volume [%s] has been updated." % (self.name) - - elif self.state == 'absent': - self.delete_volume() - msg = "Volume [%s] has been deleted." % (self.name) else: - self.debug("exiting with no changes") - if self.state == 'absent': - msg = "Volume [%s] did not exist." % (self.name) - else: - msg = "Volume [%s] already exists." % (self.name) + msg = "Volume [%s] does not exist." if self.state == 'absent' else "Volume [%s] exists." - self.module.exit_json(msg=msg, changed=changed) + self.module.exit_json(msg=(msg % self.name if msg and "%s" in msg else msg), changed=change) def main(): - v = NetAppESeriesVolume() - - try: - v.apply() - except Exception as e: - v.debug("Exception in apply(): \n%s", format_exc()) - v.module.fail_json(msg="Module failed. Error [%s]." % to_native(e)) + volume = NetAppESeriesVolume() + volume.apply() if __name__ == '__main__': diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index d5e6e261c8..f596aab174 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -782,9 +782,7 @@ lib/ansible/modules/storage/netapp/netapp_e_storage_system.py E322 lib/ansible/modules/storage/netapp/netapp_e_storage_system.py E324 lib/ansible/modules/storage/netapp/netapp_e_storagepool.py E322 lib/ansible/modules/storage/netapp/netapp_e_storagepool.py E326 -lib/ansible/modules/storage/netapp/netapp_e_volume.py E322 lib/ansible/modules/storage/netapp/netapp_e_volume.py E324 -lib/ansible/modules/storage/netapp/netapp_e_volume.py E326 lib/ansible/modules/storage/netapp/netapp_e_volume.py E327 lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E322 lib/ansible/modules/storage/netapp/netapp_e_volume_copy.py E323 diff --git a/test/units/modules/storage/netapp/test_netapp_e_volume.py b/test/units/modules/storage/netapp/test_netapp_e_volume.py new file mode 100644 index 0000000000..77d113268b --- /dev/null +++ b/test/units/modules/storage/netapp/test_netapp_e_volume.py @@ -0,0 +1,773 @@ +# coding=utf-8 +# (c) 2018, NetApp Inc. +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +try: + from unittest import mock +except ImportError: + import mock + +from ansible.module_utils.netapp import NetAppESeriesModule +from ansible.modules.storage.netapp.netapp_e_volume import NetAppESeriesVolume +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args + +__metaclass__ = type + + +class NetAppESeriesVolumeTest(ModuleTestCase): + REQUIRED_PARAMS = {"api_username": "username", + "api_password": "password", + "api_url": "http://localhost/devmgr/v2", + "ssid": "1", + "validate_certs": "no"} + + THIN_VOLUME_RESPONSE = [{"capacity": "1288490188800", + "volumeRef": "3A000000600A098000A4B28D000010475C405428", + "status": "optimal", + "protectionType": "type1Protection", + "maxVirtualCapacity": "281474976710656", + "initialProvisionedCapacity": "4294967296", + "currentProvisionedCapacity": "4294967296", + "provisionedCapacityQuota": "1305670057984", + "growthAlertThreshold": 85, + "expansionPolicy": "automatic", + "flashCached": False, + "metadata": [{"key": "workloadId", "value": "4200000001000000000000000000000000000000"}, + {"key": "volumeTypeId", "value": "volume"}], + "dataAssurance": True, + "segmentSize": 131072, + "diskPool": True, + "listOfMappings": [], + "mapped": False, + "currentControllerId": "070000000000000000000001", + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 0}, + "name": "thin_volume", + "id": "3A000000600A098000A4B28D000010475C405428"}] + VOLUME_GET_RESPONSE = [{"offline": False, + "raidLevel": "raid6", + "capacity": "214748364800", + "reconPriority": 1, + "segmentSize": 131072, + "volumeRef": "02000000600A098000A4B9D100000F095C2F7F31", + "status": "optimal", + "protectionInformationCapable": False, + "protectionType": "type0Protection", + "diskPool": True, + "flashCached": False, + "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"}, + {"key": "volumeTypeId", "value": "Clare"}], + "dataAssurance": False, + "currentControllerId": "070000000000000000000002", + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False, + "readAheadMultiplier": 0}, + "thinProvisioned": False, + "totalSizeInBytes": "214748364800", + "name": "Matthew", + "id": "02000000600A098000A4B9D100000F095C2F7F31"}, + {"offline": False, + "raidLevel": "raid6", + "capacity": "107374182400", + "reconPriority": 1, + "segmentSize": 131072, + "volumeRef": "02000000600A098000A4B28D00000FBE5C2F7F26", + "status": "optimal", + "protectionInformationCapable": False, + "protectionType": "type0Protection", + "diskPool": True, + "flashCached": False, + "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"}, + {"key": "volumeTypeId", "value": "Samantha"}], + "dataAssurance": False, + "currentControllerId": "070000000000000000000001", + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False, + "readAheadMultiplier": 0}, + "thinProvisioned": False, + "totalSizeInBytes": "107374182400", + "name": "Samantha", + "id": "02000000600A098000A4B28D00000FBE5C2F7F26"}, + {"offline": False, + "raidLevel": "raid6", + "capacity": "107374182400", + "segmentSize": 131072, + "volumeRef": "02000000600A098000A4B9D100000F0B5C2F7F40", + "status": "optimal", + "protectionInformationCapable": False, + "protectionType": "type0Protection", + "volumeGroupRef": "04000000600A098000A4B9D100000F085C2F7F26", + "diskPool": True, + "flashCached": False, + "metadata": [{"key": "workloadId", "value": "4200000002000000000000000000000000000000"}, + {"key": "volumeTypeId", "value": "Micah"}], + "dataAssurance": False, + "currentControllerId": "070000000000000000000002", + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False, + "readAheadMultiplier": 0}, + "thinProvisioned": False, + "totalSizeInBytes": "107374182400", + "name": "Micah", + "id": "02000000600A098000A4B9D100000F0B5C2F7F40"}] + STORAGE_POOL_GET_RESPONSE = [{"offline": False, + "raidLevel": "raidDiskPool", + "volumeGroupRef": "04000000600A", + "securityType": "capable", + "protectionInformationCapable": False, + "protectionInformationCapabilities": {"protectionInformationCapable": True, + "protectionType": "type2Protection"}, + "volumeGroupData": {"type": "diskPool", + "diskPoolData": {"reconstructionReservedDriveCount": 1, + "reconstructionReservedAmt": "296889614336", + "reconstructionReservedDriveCountCurrent": 1, + "poolUtilizationWarningThreshold": 0, + "poolUtilizationCriticalThreshold": 85, + "poolUtilizationState": "utilizationOptimal", + "unusableCapacity": "0", + "degradedReconstructPriority": "high", + "criticalReconstructPriority": "highest", + "backgroundOperationPriority": "low", + "allocGranularity": "4294967296"}}, + "reservedSpaceAllocated": False, + "securityLevel": "fde", + "usedSpace": "863288426496", + "totalRaidedSpace": "2276332666880", + "raidStatus": "optimal", + "freeSpace": "1413044240384", + "drivePhysicalType": "sas", + "driveMediaType": "hdd", + "diskPool": True, + "id": "04000000600A098000A4B9D100000F085C2F7F26", + "name": "employee_data_storage_pool"}, + {"offline": False, + "raidLevel": "raid1", + "volumeGroupRef": "04000000600A098000A4B28D00000FBD5C2F7F19", + "state": "complete", + "securityType": "capable", + "drawerLossProtection": False, + "protectionInformationCapable": False, + "protectionInformationCapabilities": {"protectionInformationCapable": True, + "protectionType": "type2Protection"}, + "volumeGroupData": {"type": "unknown", "diskPoolData": None}, + "reservedSpaceAllocated": False, + "securityLevel": "fde", + "usedSpace": "322122547200", + "totalRaidedSpace": "598926258176", + "raidStatus": "optimal", + "freeSpace": "276803710976", + "drivePhysicalType": "sas", + "driveMediaType": "hdd", + "diskPool": False, + "id": "04000000600A098000A4B28D00000FBD5C2F7F19", + "name": "database_storage_pool"}] + REQUEST_FUNC = "ansible.modules.storage.netapp.netapp_e_volume.NetAppESeriesVolume.request" + + WORKLOAD_GET_RESPONSE = [{"id": "4200000001000000000000000000000000000000", "name": "general_workload_1", + "workloadAttributes": [{"key": "profileId", "value": "Other_1"}]}, + {"id": "4200000002000000000000000000000000000000", "name": "employee_data", + "workloadAttributes": [{"key": "use", "value": "EmployeeData"}, + {"key": "location", "value": "ICT"}, + {"key": "private", "value": "public"}, + {"key": "profileId", "value": "ansible_workload_1"}]}, + {"id": "4200000003000000000000000000000000000000", "name": "customer_database", + "workloadAttributes": [{"key": "use", "value": "customer_information"}, + {"key": "location", "value": "global"}, + {"key": "profileId", "value": "ansible_workload_2"}]}, + {"id": "4200000004000000000000000000000000000000", "name": "product_database", + "workloadAttributes": [{"key": "use", "value": "production_information"}, + {"key": "security", "value": "private"}, + {"key": "location", "value": "global"}, + {"key": "profileId", "value": "ansible_workload_4"}]}] + + def _set_args(self, args=None): + module_args = self.REQUIRED_PARAMS.copy() + if args is not None: + module_args.update(args) + set_module_args(module_args) + + def test_module_arguments_pass(self): + """Ensure valid arguments successful create a class instance.""" + arg_sets = [{"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 10}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "gb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1024, + "thin_volume_growth_alert_threshold": 99}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "gb", + "thin_provision": True, "thin_volume_repo_size": 64}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "kb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 67108864}] + + # validate size normalization + for arg_set in arg_sets: + self._set_args(arg_set) + volume_object = NetAppESeriesVolume() + + size_unit_multiplier = NetAppESeriesModule.SIZE_UNIT_MAP[arg_set["size_unit"]] + self.assertEqual(volume_object.size_b, arg_set["size"] * size_unit_multiplier) + self.assertEqual(volume_object.thin_volume_repo_size_b, + arg_set["thin_volume_repo_size"] * size_unit_multiplier) + self.assertEqual(volume_object.thin_volume_expansion_policy, "automatic") + if "thin_volume_max_repo_size" not in arg_set.keys(): + self.assertEqual(volume_object.thin_volume_max_repo_size_b, arg_set["size"] * size_unit_multiplier) + else: + self.assertEqual(volume_object.thin_volume_max_repo_size_b, + arg_set["thin_volume_max_repo_size"] * size_unit_multiplier) + + # validate metadata form + self._set_args( + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10, "workload_name": "workload1", + "metadata": {"availability": "public", "security": "low"}}) + volume_object = NetAppESeriesVolume() + for entry in volume_object.metadata: + self.assertTrue(entry in [{'value': 'low', 'key': 'security'}, {'value': 'public', 'key': 'availability'}]) + + def test_module_arguments_fail(self): + """Ensure invalid arguments values do not create a class instance.""" + arg_sets = [{"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb", + "thin_provision": True, "thin_volume_repo_size": 260}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "tb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 10}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "gb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 9}, + {"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 10000, "size_unit": "gb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 100}] + + for arg_set in arg_sets: + with self.assertRaises(AnsibleFailJson): + self._set_args(arg_set) + print(arg_set) + volume_object = NetAppESeriesVolume() + + def test_get_volume_pass(self): + """Evaluate the get_volume method.""" + with mock.patch(self.REQUEST_FUNC, + side_effect=[(200, self.VOLUME_GET_RESPONSE), (200, self.THIN_VOLUME_RESPONSE)]): + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + self.assertEqual(volume_object.get_volume(), + [entry for entry in self.VOLUME_GET_RESPONSE if entry["name"] == "Matthew"][0]) + + with mock.patch(self.REQUEST_FUNC, + side_effect=[(200, self.VOLUME_GET_RESPONSE), (200, self.THIN_VOLUME_RESPONSE)]): + self._set_args({"state": "present", "name": "NotAVolume", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + self.assertEqual(volume_object.get_volume(), {}) + + def test_get_volume_fail(self): + """Evaluate the get_volume exception paths.""" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of thick volumes."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + volume_object.get_volume() + + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of thin volumes."): + with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.VOLUME_GET_RESPONSE), Exception()]): + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + volume_object.get_volume() + + def test_get_storage_pool_pass(self): + """Evaluate the get_storage_pool method.""" + with mock.patch(self.REQUEST_FUNC, return_value=(200, self.STORAGE_POOL_GET_RESPONSE)): + self._set_args({"state": "present", "name": "NewVolume", "storage_pool_name": "employee_data_storage_pool", + "size": 100}) + volume_object = NetAppESeriesVolume() + self.assertEqual(volume_object.get_storage_pool(), [entry for entry in self.STORAGE_POOL_GET_RESPONSE if + entry["name"] == "employee_data_storage_pool"][0]) + + self._set_args( + {"state": "present", "name": "NewVolume", "storage_pool_name": "NotAStoragePool", "size": 100}) + volume_object = NetAppESeriesVolume() + self.assertEqual(volume_object.get_storage_pool(), {}) + + def test_get_storage_pool_fail(self): + """Evaluate the get_storage_pool exception paths.""" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to obtain list of storage pools."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + volume_object.get_storage_pool() + + def test_check_storage_pool_sufficiency_pass(self): + """Ensure passing logic.""" + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = [entry for entry in self.STORAGE_POOL_GET_RESPONSE + if entry["name"] == "employee_data_storage_pool"][0] + volume_object.check_storage_pool_sufficiency() + + def test_check_storage_pool_sufficiency_fail(self): + """Validate exceptions are thrown for insufficient storage pool resources.""" + self._set_args({"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb", + "thin_provision": True, "thin_volume_repo_size": 64, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 10}) + volume_object = NetAppESeriesVolume() + + with self.assertRaisesRegexp(AnsibleFailJson, "Requested storage pool"): + volume_object.check_storage_pool_sufficiency() + + with self.assertRaisesRegexp(AnsibleFailJson, + "Thin provisioned volumes can only be created on raid disk pools."): + volume_object.pool_detail = [entry for entry in self.STORAGE_POOL_GET_RESPONSE + if entry["name"] == "database_storage_pool"][0] + volume_object.volume_detail = {} + volume_object.check_storage_pool_sufficiency() + + with self.assertRaisesRegexp(AnsibleFailJson, "requires the storage pool to be DA-compatible."): + volume_object.pool_detail = {"diskPool": True, + "protectionInformationCapabilities": {"protectionType": "type0Protection", + "protectionInformationCapable": False}} + volume_object.volume_detail = {} + volume_object.data_assurance_enabled = True + volume_object.check_storage_pool_sufficiency() + + volume_object.pool_detail = {"diskPool": True, + "protectionInformationCapabilities": {"protectionType": "type2Protection", + "protectionInformationCapable": True}} + volume_object.check_storage_pool_sufficiency() + + self._set_args({"state": "present", "name": "vol", "storage_pool_name": "pool", "size": 100, "size_unit": "tb", + "thin_provision": False}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, + "Not enough storage pool free space available for the volume's needs."): + volume_object.pool_detail = {"freeSpace": 10, "diskPool": True, + "protectionInformationCapabilities": {"protectionType": "type2Protection", + "protectionInformationCapable": True}} + volume_object.volume_detail = {"totalSizeInBytes": 100} + volume_object.data_assurance_enabled = True + volume_object.size_b = 1 + volume_object.check_storage_pool_sufficiency() + + def test_update_workload_tags_pass(self): + """Validate updating workload tags.""" + test_sets = [[{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100}, False], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "employee_data"}, False], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "customer_database", + "metadata": {"use": "customer_information", "location": "global"}}, False], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "customer_database", + "metadata": {"use": "customer_information"}}, True], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "customer_database", + "metadata": {"use": "customer_information", "location": "local"}}, True], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "customer_database", + "metadata": {"use": "customer_information", "location": "global", "importance": "no"}}, True], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "newWorkload", + "metadata": {"for_testing": "yes"}}, True], + [{"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "newWorkload"}, True]] + + for test in test_sets: + self._set_args(test[0]) + volume_object = NetAppESeriesVolume() + + with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), (200, {"id": 1})]): + self.assertEqual(volume_object.update_workload_tags(), test[1]) + + def test_update_workload_tags_fail(self): + """Validate updating workload tags fails appropriately.""" + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "employee_data"}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to retrieve storage array workload tags."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.update_workload_tags() + + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "employee_data", "metadata": {"key": "not-use", "value": "EmployeeData"}}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create new workload tag."): + with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), Exception()]): + volume_object.update_workload_tags() + + self._set_args({"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, + "workload_name": "employee_data2", "metadata": {"key": "use", "value": "EmployeeData"}}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create new workload tag."): + with mock.patch(self.REQUEST_FUNC, side_effect=[(200, self.WORKLOAD_GET_RESPONSE), Exception()]): + volume_object.update_workload_tags() + + def test_get_volume_property_changes_pass(self): + """Verify correct dictionary is returned""" + + # no property changes + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 1}, "flashCached": True, + "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), dict()) + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": True, "thin_volume_repo_size": 64, + "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 1}, + "flashCached": True, "growthAlertThreshold": "90", + "expansionPolicy": "automatic", "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), dict()) + + # property changes + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": False, "writeCacheEnable": True, + "readAheadMultiplier": 1}, "flashCached": True, + "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), + {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, + 'flashCache': True}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": False, + "readAheadMultiplier": 1}, "flashCached": True, + "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), + {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, + 'flashCache': True}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 1}, "flashCached": False, + "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), + {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, + 'flashCache': True}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": False, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 1}, "flashCached": False, + "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), {"metaTags": [], + 'cacheSettings': {'readCacheEnable': True, + 'writeCacheEnable': True, + 'readAheadEnable': False}, + 'flashCache': True}) + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, + "read_ahead_enable": True, "thin_provision": True, "thin_volume_repo_size": 64, + "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"metadata": [], + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, + "readAheadMultiplier": 1}, + "flashCached": True, "growthAlertThreshold": "95", + "expansionPolicy": "automatic", "segmentSize": str(128 * 1024)} + self.assertEqual(volume_object.get_volume_property_changes(), + {"metaTags": [], 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, + 'growthAlertThreshold': 90, 'flashCache': True}) + + def test_get_volume_property_changes_fail(self): + """Verify correct exception is thrown""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "ssd_cache_enabled": True, + "read_cache_enable": True, "write_cache_enable": True, "read_ahead_enable": True, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = { + "cacheSettings": {"readCacheEnable": True, "writeCacheEnable": True, "readAheadMultiplier": 1}, + "flashCached": True, "segmentSize": str(512 * 1024)} + with self.assertRaisesRegexp(AnsibleFailJson, "Existing volume segment size is"): + volume_object.get_volume_property_changes() + + def test_get_expand_volume_changes_pass(self): + """Verify expansion changes.""" + # thick volumes + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(50 * 1024 * 1024 * 1024), "thinProvisioned": False} + self.assertEqual(volume_object.get_expand_volume_changes(), + {"sizeUnit": "bytes", "expansionSize": 100 * 1024 * 1024 * 1024}) + + # thin volumes + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "automatic", "thin_volume_repo_size": 64, + "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(50 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "automatic", + "provisionedCapacityQuota": str(1000 * 1024 * 1024 * 1024)} + self.assertEqual(volume_object.get_expand_volume_changes(), + {"sizeUnit": "bytes", "newVirtualSize": 100 * 1024 * 1024 * 1024}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "automatic", "thin_volume_repo_size": 64, + "thin_volume_max_repo_size": 1000, "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "automatic", + "provisionedCapacityQuota": str(500 * 1024 * 1024 * 1024)} + self.assertEqual(volume_object.get_expand_volume_changes(), + {"sizeUnit": "bytes", "newRepositorySize": 1000 * 1024 * 1024 * 1024}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 504, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "manual", + "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)} + self.assertEqual(volume_object.get_expand_volume_changes(), + {"sizeUnit": "bytes", "newRepositorySize": 504 * 1024 * 1024 * 1024}) + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 756, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "manual", + "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)} + self.assertEqual(volume_object.get_expand_volume_changes(), + {"sizeUnit": "bytes", "newRepositorySize": 756 * 1024 * 1024 * 1024}) + + def test_get_expand_volume_changes_fail(self): + """Verify exceptions are thrown.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(1000 * 1024 * 1024 * 1024)} + with self.assertRaisesRegexp(AnsibleFailJson, "Reducing the size of volumes is not permitted."): + volume_object.get_expand_volume_changes() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 502, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "manual", + "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)} + with self.assertRaisesRegexp(AnsibleFailJson, "The thin volume repository increase must be between or equal"): + volume_object.get_expand_volume_changes() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"capacity": str(100 * 1024 * 1024 * 1024), "thinProvisioned": True, + "expansionPolicy": "manual", + "currentProvisionedCapacity": str(500 * 1024 * 1024 * 1024)} + with self.assertRaisesRegexp(AnsibleFailJson, "The thin volume repository increase must be between or equal"): + volume_object.get_expand_volume_changes() + + def test_create_volume_pass(self): + """Verify volume creation.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.create_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.create_volume() + + def test_create_volume_fail(self): + """Verify exceptions thrown.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.create_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to create thin volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.create_volume() + + def test_update_volume_properties_pass(self): + """verify property update.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + volume_object.wait_for_volume_availability = lambda: None + volume_object.get_volume = lambda: {"id": "12345'"} + volume_object.get_volume_property_changes = lambda: { + 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90, + 'flashCached': True} + volume_object.workload_id = "4200000001000000000000000000000000000000" + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + self.assertTrue(volume_object.update_volume_properties()) + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + volume_object.wait_for_volume_availability = lambda: None + volume_object.get_volume = lambda: {"id": "12345'"} + volume_object.get_volume_property_changes = lambda: { + 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90, + 'flashCached': True} + volume_object.workload_id = "4200000001000000000000000000000000000000" + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + self.assertTrue(volume_object.update_volume_properties()) + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"metadata": [{"key": "workloadId", "value": "12345"}]} + volume_object.wait_for_volume_availability = lambda: None + volume_object.get_volume = lambda: {"id": "12345'"} + volume_object.get_volume_property_changes = lambda: {} + volume_object.workload_id = "4200000001000000000000000000000000000000" + self.assertFalse(volume_object.update_volume_properties()) + + def test_update_volume_properties_fail(self): + """Verify exceptions are thrown.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + volume_object.wait_for_volume_availability = lambda: None + volume_object.get_volume = lambda: {"id": "12345'"} + volume_object.get_volume_property_changes = lambda: { + 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90, + 'flashCached': True} + volume_object.workload_id = "4200000001000000000000000000000000000000" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update volume properties."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + self.assertTrue(volume_object.update_volume_properties()) + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.pool_detail = {"id": "12345"} + volume_object.wait_for_volume_availability = lambda: None + volume_object.get_volume = lambda: {"id": "12345'"} + volume_object.get_volume_property_changes = lambda: { + 'cacheSettings': {'readCacheEnable': True, 'writeCacheEnable': True}, 'growthAlertThreshold': 90, + 'flashCached': True} + volume_object.workload_id = "4200000001000000000000000000000000000000" + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to update thin volume properties."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + self.assertTrue(volume_object.update_volume_properties()) + + def test_expand_volume_pass(self): + """Verify volume expansion.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes", + "expansionSize": 100 * 1024 * 1024 * 1024} + volume_object.volume_detail = {"id": "12345", "thinProvisioned": True} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.expand_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes", + "expansionSize": 100 * 1024 * 1024 * 1024} + volume_object.volume_detail = {"id": "12345", "thinProvisioned": True} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.expand_volume() + + def test_expand_volume_fail(self): + """Verify exceptions are thrown.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes", + "expansionSize": 100 * 1024 * 1024 * 1024} + volume_object.volume_detail = {"id": "12345", "thinProvisioned": False} + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to expand volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.expand_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True}) + volume_object = NetAppESeriesVolume() + volume_object.get_expand_volume_changes = lambda: {"sizeUnit": "bytes", + "expansionSize": 100 * 1024 * 1024 * 1024} + volume_object.volume_detail = {"id": "12345", "thinProvisioned": True} + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to expand thin volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.expand_volume() + + def test_delete_volume_pass(self): + """Verify volume deletion.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"id": "12345"} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.delete_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True, + "thin_volume_expansion_policy": "manual", "thin_volume_repo_size": 760, "thin_volume_max_repo_size": 1000, + "thin_volume_growth_alert_threshold": 90}) + volume_object = NetAppESeriesVolume() + volume_object.volume_detail = {"id": "12345"} + with mock.patch(self.REQUEST_FUNC, return_value=(200, {})): + volume_object.delete_volume() + + def test_delete_volume_fail(self): + """Verify exceptions are thrown.""" + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": False}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to delete volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.delete_volume() + + self._set_args( + {"state": "present", "name": "Matthew", "storage_pool_name": "pool", "size": 100, "thin_provision": True}) + volume_object = NetAppESeriesVolume() + with self.assertRaisesRegexp(AnsibleFailJson, "Failed to delete thin volume."): + with mock.patch(self.REQUEST_FUNC, return_value=Exception()): + volume_object.delete_volume()