#!/usr/bin/python # Copyright: (c) 2020, Dell Technologies # Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) """Ansible module for managing volumes on Unity""" from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = r""" module: volume version_added: '1.1.0' short_description: Manage volume on Unity storage system description: - Managing volume on Unity storage system includes- Create new volume, Modify volume attributes, Map Volume to host, Unmap volume to host, Display volume details, Delete volume. extends_documentation_fragment: - dellemc.unity.unity author: - Arindam Datta (@arindam-emc) - Pavan Mudunuri(@Pavan-Mudunuri) options: vol_name: description: - The name of the volume. Mandatory only for create operation. type: str vol_id: description: - The id of the volume. - It can be used only for get, modify, map/unmap host, or delete operation. type: str pool_name: description: - This is the name of the pool where the volume will be created. - Either the I(pool_name) or I(pool_id) must be provided to create a new volume. type: str pool_id: description: - This is the id of the pool where the volume will be created. - Either the I(pool_name) or I(pool_id) must be provided to create a new volume. type: str size: description: - The size of the volume. type: int cap_unit: description: - The unit of the volume size. It defaults to C(GB), if not specified. choices: ['GB' , 'TB'] type: str description: description: - Description about the volume. - Description can be removed by passing empty string (""). type: str snap_schedule: description: - Snapshot schedule assigned to the volume. - Add/Remove/Modify the snapshot schedule for the volume. type: str compression: description: - Boolean variable, Specifies whether or not to enable compression. Compression is supported only for thin volumes. type: bool advanced_dedup: description: - Boolean variable, Indicates whether or not to enable advanced deduplication. - Compression should be enabled to enable advanced deduplication. - It can only be enabled on the all flash high end platforms. - Deduplicated data will remain as is even after advanced deduplication is disabled. type: bool is_thin: description: - Boolean variable, Specifies whether or not it is a thin volume. - The value is set as C(true) by default if not specified. type: bool sp: description: - Storage Processor for this volume. choices: ['SPA' , 'SPB'] type: str io_limit_policy: description: - IO limit policy associated with this volume. Once it is set, it cannot be removed through ansible module but it can be changed. type: str host_name: description: - Name of the host to be mapped/unmapped with this volume. - Either I(host_name) or I(host_id) can be specified in one task along with I(mapping_state). type: str host_id: description: - ID of the host to be mapped/unmapped with this volume. - Either I(host_name) or I(host_id) can be specified in one task along with I(mapping_state). type: str hlu: description: - Host Lun Unit to be mapped/unmapped with this volume. - It is an optional parameter, hlu can be specified along with I(host_name) or I(host_id) and I(mapping_state). - If I(hlu) is not specified, unity will choose it automatically. The maximum value supported is C(255). type: int mapping_state: description: - State of host access for volume. choices: ['mapped' , 'unmapped'] type: str new_vol_name: description: - New name of the volume for rename operation. type: str tiering_policy: description: - Tiering policy choices for how the storage resource data will be distributed among the tiers available in the pool. choices: ['AUTOTIER_HIGH', 'AUTOTIER', 'HIGHEST', 'LOWEST'] type: str state: description: - State variable to determine whether volume will exist or not. choices: ['absent', 'present'] required: true type: str hosts: description: - Name of hosts for mapping to a volume. type: list elements: dict suboptions: host_name: description: - Name of the host. type: str host_id: description: - ID of the host. type: str hlu: description: - Host Lun Unit to be mapped/unmapped with this volume. - It is an optional parameter, I(hlu) can be specified along with I(host_name) or I(host_id) and I(mapping_state). - If I(hlu) is not specified, unity will choose it automatically. The maximum value supported is C(255). type: str notes: - The I(check_mode) is not supported. """ EXAMPLES = r""" - name: Create Volume dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_name: "{{vol_name}}" description: "{{description}}" pool_name: "{{pool}}" size: 2 cap_unit: "{{cap_GB}}" is_thin: True compression: True advanced_dedup: True state: "{{state_present}}" - name: Expand Volume by volume id dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_id: "{{vol_id}}" size: 5 cap_unit: "{{cap_GB}}" state: "{{state_present}}" - name: Modify Volume, map host by host_name dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_name: "{{vol_name}}" host_name: "{{host_name}}" hlu: 5 mapping_state: "{{state_mapped}}" state: "{{state_present}}" - name: Modify Volume, unmap host mapping by host_name dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_name: "{{vol_name}}" host_name: "{{host_name}}" mapping_state: "{{state_unmapped}}" state: "{{state_present}}" - name: Map multiple hosts to a Volume dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_id: "{{vol_id}}" hosts: - host_name: "10.226.198.248" hlu: 1 - host_id: "Host_929" hlu: 2 mapping_state: "mapped" state: "present" - name: Modify Volume attributes dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_name: "{{vol_name}}" new_vol_name: "{{new_vol_name}}" tiering_policy: "AUTOTIER" compression: True is_thin: True advanced_dedup: True state: "{{state_present}}" - name: Delete Volume by vol name dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_name: "{{vol_name}}" state: "{{state_absent}}" - name: Delete Volume by vol id dellemc.unity.volume: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" vol_id: "{{vol_id}}" state: "{{state_absent}}" """ RETURN = r''' changed: description: Whether or not the resource has changed. returned: always type: bool sample: True volume_details: description: Details of the volume. returned: When volume exists type: dict contains: id: description: The system generated ID given to the volume. type: str name: description: Name of the volume. type: str description: description: Description about the volume. type: str is_data_reduction_enabled: description: Whether or not compression enabled on this volume. type: bool size_total_with_unit: description: Size of the volume with actual unit. type: str snap_schedule: description: Snapshot schedule applied to this volume. type: dict tiering_policy: description: Tiering policy applied to this volume. type: str current_sp: description: Current storage processor for this volume. type: str pool: description: The pool in which this volume is allocated. type: dict host_access: description: Host mapped to this volume. type: list io_limit_policy: description: IO limit policy associated with this volume. type: dict wwn: description: The world wide name of this volume. type: str is_thin_enabled: description: Indicates whether thin provisioning is enabled for this volume. type: bool sample: { "current_node": "NodeEnum.SPB", "data_reduction_percent": 0, "data_reduction_ratio": 1.0, "data_reduction_size_saved": 0, "default_node": "NodeEnum.SPB", "description": null, "effective_io_limit_max_iops": null, "effective_io_limit_max_kbps": null, "existed": true, "family_base_lun": { "UnityLun": { "hash": 8774954523796, "id": "sv_27" } }, "family_clone_count": 0, "hash": 8774954522426, "health": { "UnityHealth": { "hash": 8774954528278 } }, "host_access": [ { "accessMask": "PRODUCTION", "hlu": 0, "id": "Host_75", "name": "10.226.198.250" } ], "id": "sv_27", "io_limit_policy": null, "is_advanced_dedup_enabled": false, "is_compression_enabled": null, "is_data_reduction_enabled": false, "is_replication_destination": false, "is_snap_schedule_paused": false, "is_thin_clone": false, "is_thin_enabled": false, "metadata_size": 4294967296, "metadata_size_allocated": 4026531840, "name": "VSI-UNITY-test-task", "per_tier_size_used": [ 111400714240, 0, 0 ], "pool": { "id": "pool_3", "name": "Extreme_Perf_tier" }, "size_allocated": 107374182400, "size_total": 107374182400, "size_total_with_unit": "100.0 GB", "size_used": null, "snap_count": 0, "snap_schedule": null, "snap_wwn": "60:06:01:60:5C:F0:50:00:94:3E:91:4D:51:5A:4F:97", "snaps_size": 0, "snaps_size_allocated": 0, "storage_resource": { "UnityStorageResource": { "hash": 8774954518887 } }, "tiering_policy": "TieringPolicyEnum.AUTOTIER_HIGH", "type": "LUNTypeEnum.VMWARE_ISCSI", "wwn": "60:06:01:60:5C:F0:50:00:00:B5:95:61:2E:34:DB:B2" } ''' from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \ import utils import logging LOG = utils.get_logger('volume') application_type = "Ansible/1.6.0" def is_none_or_empty_string(param): """ validates the input string for None or empty values """ return not param or len(str(param)) <= 0 class Volume(object): """Class with volume operations""" param_host_id = None param_io_limit_pol_id = None param_snap_schedule_name = None def __init__(self): """Define all parameters required by this module""" self.module_params = utils.get_unity_management_host_parameters() self.module_params.update(get_volume_parameters()) mutually_exclusive = [['vol_name', 'vol_id'], ['pool_name', 'pool_id'], ['host_name', 'host_id']] required_one_of = [['vol_name', 'vol_id']] # initialize the Ansible module self.module = AnsibleModule( argument_spec=self.module_params, supports_check_mode=False, mutually_exclusive=mutually_exclusive, required_one_of=required_one_of) utils.ensure_required_libs(self.module) self.unity_conn = utils.get_unity_unisphere_connection( self.module.params, application_type) def get_volume(self, vol_name=None, vol_id=None): """Get the details of a volume. :param vol_name: The name of the volume :param vol_id: The id of the volume :return: instance of the respective volume if exist. """ id_or_name = vol_id if vol_id else vol_name errormsg = "Failed to get the volume {0} with error {1}" try: obj_vol = self.unity_conn.get_lun(name=vol_name, _id=vol_id) if vol_id and obj_vol.existed: LOG.info("Successfully got the volume object %s ", obj_vol) return obj_vol elif vol_name: LOG.info("Successfully got the volume object %s ", obj_vol) return obj_vol else: LOG.info("Failed to get the volume %s", id_or_name) return None except utils.HttpError as e: if e.http_status == 401: cred_err = "Incorrect username or password , {0}".format( e.message) msg = errormsg.format(id_or_name, cred_err) self.module.fail_json(msg=msg) else: msg = errormsg.format(id_or_name, str(e)) self.module.fail_json(msg=msg) except utils.UnityResourceNotFoundError as e: msg = errormsg.format(id_or_name, str(e)) LOG.error(msg) return None except Exception as e: msg = errormsg.format(id_or_name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_host(self, host_name=None, host_id=None): """Get the instance of a host. :param host_name: The name of the host :param host_id: The id of the volume :return: instance of the respective host if exist. """ id_or_name = host_id if host_id else host_name errormsg = "Failed to get the host {0} with error {1}" try: obj_host = self.unity_conn.get_host(name=host_name, _id=host_id) if host_id and obj_host.existed: LOG.info("Successfully got the host object %s ", obj_host) return obj_host elif host_name: LOG.info("Successfully got the host object %s ", obj_host) return obj_host else: msg = "Failed to get the host {0}".format(id_or_name) LOG.error(msg) self.module.fail_json(msg=msg) except Exception as e: msg = errormsg.format(id_or_name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_snap_schedule(self, name): """Get the instance of a snapshot schedule. :param name: The name of the snapshot schedule :return: instance of the respective snapshot schedule if exist. """ errormsg = "Failed to get the snapshot schedule {0} with error {1}" try: LOG.debug("Attempting to get Snapshot Schedule with name %s", name) obj_ss = utils.UnitySnapScheduleList.get(self.unity_conn._cli, name=name) if obj_ss and (len(obj_ss) > 0): LOG.info("Successfully got Snapshot Schedule %s", obj_ss) return obj_ss else: msg = "Failed to get snapshot schedule " \ "with name {0}".format(name) LOG.error(msg) self.module.fail_json(msg=msg) except Exception as e: msg = errormsg.format(name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_io_limit_policy(self, name=None, id=None): """Get the instance of a io limit policy. :param name: The io limit policy name :param id: The io limit policy id :return: instance of the respective io_limit_policy if exist. """ errormsg = "Failed to get the io limit policy {0} with error {1}" id_or_name = name if name else id try: obj_iopol = self.unity_conn.get_io_limit_policy(_id=id, name=name) if id and obj_iopol.existed: LOG.info("Successfully got the IO limit policy object %s", obj_iopol) return obj_iopol elif name: LOG.info("Successfully got the IO limit policy object %s ", obj_iopol) return obj_iopol else: msg = "Failed to get the io limit policy with {0}".format( id_or_name) LOG.error(msg) self.module.fail_json(msg=msg) except Exception as e: msg = errormsg.format(name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_pool(self, pool_name=None, pool_id=None): """Get the instance of a pool. :param pool_name: The name of the pool :param pool_id: The id of the pool :return: Dict containing pool details if exists """ id_or_name = pool_id if pool_id else pool_name errormsg = "Failed to get the pool {0} with error {1}" try: obj_pool = self.unity_conn.get_pool(name=pool_name, _id=pool_id) if pool_id and obj_pool.existed: LOG.info("Successfully got the pool object %s", obj_pool) return obj_pool if pool_name: LOG.info("Successfully got pool %s", obj_pool) return obj_pool else: msg = "Failed to get the pool with " \ "{0}".format(id_or_name) LOG.error(msg) self.module.fail_json(msg=msg) except Exception as e: msg = errormsg.format(id_or_name, str(e)) LOG.error(msg) self.module.fail_json(msg=msg) def get_node_enum(self, sp): """Get the storage processor enum. :param sp: The storage processor string :return: storage processor enum """ if sp in utils.NodeEnum.__members__: return utils.NodeEnum[sp] else: errormsg = "Invalid choice {0} for storage processor".format( sp) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_tiering_policy_enum(self, tiering_policy): """Get the tiering_policy enum. :param tiering_policy: The tiering_policy string :return: tiering_policy enum """ if tiering_policy in utils.TieringPolicyEnum.__members__: return utils.TieringPolicyEnum[tiering_policy] else: errormsg = "Invalid choice {0} for tiering policy".format( tiering_policy) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def create_volume(self, obj_pool, size, host_access=None): """Create a volume. :param obj_pool: pool object instance :param size: size of the volume in GB :param host_access: host to be associated with this volume :return: Volume object on successful creation """ vol_name = self.module.params['vol_name'] try: description = self.module.params['description'] compression = self.module.params['compression'] advanced_dedup = self.module.params['advanced_dedup'] is_thin = self.module.params['is_thin'] snap_schedule = None sp = self.module.params['sp'] sp = self.get_node_enum(sp) if sp else None io_limit_policy = self.get_io_limit_policy( id=self.param_io_limit_pol_id) \ if self.module.params['io_limit_policy'] else None if self.param_snap_schedule_name: snap_schedule = {"name": self.param_snap_schedule_name} tiering_policy = self.module.params['tiering_policy'] tiering_policy = self.get_tiering_policy_enum(tiering_policy) \ if tiering_policy else None obj_vol = obj_pool.create_lun(lun_name=vol_name, size_gb=size, sp=sp, host_access=host_access, is_thin=is_thin, description=description, tiering_policy=tiering_policy, snap_schedule=snap_schedule, io_limit_policy=io_limit_policy, is_compression=compression, is_advanced_dedup_enabled=advanced_dedup) LOG.info("Successfully created volume , %s", obj_vol) return obj_vol except Exception as e: errormsg = "Create volume operation {0} failed" \ " with error {1}".format(vol_name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def host_access_modify_required(self, host_access_list): """Check if host access modification is required :param host_access_list: host access dict list :return: Dict with attributes to modify, or None if no modification is required. """ try: to_modify = False mapping_state = self.module.params['mapping_state'] host_id_list = [] hlu_list = [] new_list = [] if not host_access_list and self.new_host_list and\ mapping_state == 'unmapped': return to_modify elif host_access_list: for host_access in host_access_list.host: host_id_list.append(host_access.id) host = self.get_host(host_id=host_access.id).update() host_dict = host.host_luns._get_properties() LOG.debug("check if hlu present : %s", host_dict) if "hlu" in host_dict.keys(): hlu_list.append(host_dict['hlu']) if mapping_state == 'mapped': if (self.param_host_id not in host_id_list): for item in self.new_host_list: new_list.append(item.get("host_id")) if not list(set(new_list) - set(host_id_list)): return False to_modify = True if mapping_state == 'unmapped': if self.new_host_list: for item in self.new_host_list: new_list.append(item.get("host_id")) if list(set(new_list) - set(host_id_list)): return False self.overlapping_list = list(set(host_id_list) - set(new_list)) to_modify = True LOG.debug("host_access_modify_required : %s ", str(to_modify)) return to_modify except Exception as e: errormsg = "Failed to compare the host_access with error {0} " \ "{1}".format(host_access_list, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def volume_modify_required(self, obj_vol, cap_unit): """Check if volume modification is required :param obj_vol: volume instance :param cap_unit: capacity unit :return: Boolean value to indicate if modification is required """ try: to_update = {} new_vol_name = self.module.params['new_vol_name'] if new_vol_name and obj_vol.name != new_vol_name: to_update.update({'name': new_vol_name}) description = self.module.params['description'] if description and obj_vol.description != description: to_update.update({'description': description}) size = self.module.params['size'] if size and cap_unit: size_byte = int(utils.get_size_bytes(size, cap_unit)) if size_byte < obj_vol.size_total: self.module.fail_json(msg="Volume size can be " "expanded only") elif size_byte > obj_vol.size_total: to_update.update({'size': size_byte}) compression = self.module.params['compression'] if compression is not None and \ compression != obj_vol.is_data_reduction_enabled: to_update.update({'is_compression': compression}) advanced_dedup = self.module.params['advanced_dedup'] if advanced_dedup is not None and \ advanced_dedup != obj_vol.is_advanced_dedup_enabled: to_update.update({'is_advanced_dedup_enabled': advanced_dedup}) is_thin = self.module.params['is_thin'] if is_thin is not None and is_thin != obj_vol.is_thin_enabled: self.module.fail_json(msg="Modifying is_thin is not allowed") sp = self.module.params['sp'] if sp and self.get_node_enum(sp) != obj_vol.current_node: to_update.update({'sp': self.get_node_enum(sp)}) tiering_policy = self.module.params['tiering_policy'] if tiering_policy and self.get_tiering_policy_enum( tiering_policy) != obj_vol.tiering_policy: to_update.update({'tiering_policy': self.get_tiering_policy_enum( tiering_policy)}) # prepare io_limit_policy object if self.param_io_limit_pol_id: if (not obj_vol.io_limit_policy) \ or (self.param_io_limit_pol_id != obj_vol.io_limit_policy.id): to_update.update( {'io_limit_policy': self.param_io_limit_pol_id}) # prepare snap_schedule object if self.param_snap_schedule_name: if (not obj_vol.snap_schedule) \ or (self.param_snap_schedule_name != obj_vol.snap_schedule.name): to_update.update({'snap_schedule': self.param_snap_schedule_name}) # for removing existing snap_schedule if self.param_snap_schedule_name == "": if obj_vol.snap_schedule: to_update.update({'is_snap_schedule_paused': False}) else: LOG.warn("No snapshot schedule is associated") LOG.debug("Volume to modify Dict : %s", to_update) if len(to_update) > 0: return to_update else: return None except Exception as e: errormsg = "Failed to determine if volume {0},requires " \ "modification, with error {1}".format(obj_vol.name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def multiple_host_map(self, host_dic_list, obj_vol): """Attach multiple hosts to a volume :param host_dic_list: hosts to map the volume :param obj_vol: volume instance :return: response from API call """ try: host_access = [] current_hosts = self.get_volume_host_access_list(obj_vol) for existing_host in current_hosts: host_access.append( {'accessMask': eval('utils.HostLUNAccessEnum.' + existing_host['accessMask']), 'host': {'id': existing_host['id']}, 'hlu': existing_host['hlu']}) for item in host_dic_list: host_access.append( {'accessMask': utils.HostLUNAccessEnum.PRODUCTION, 'host': {'id': item['host_id']}, 'hlu': item['hlu']}) resp = obj_vol.modify(host_access=host_access) return resp except Exception as e: errormsg = "Failed to attach hosts {0} with volume {1} with error {2} ".format(host_dic_list, obj_vol.name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def multiple_detach(self, host_list_detach, obj_vol): """Detach multiple hosts from a volume :param host_list_detach: hosts to unmap the volume :param obj_vol: volume instance :return: response from API call """ try: host_access = [] for item in host_list_detach: host_access.append({'accessMask': utils.HostLUNAccessEnum.PRODUCTION, 'host': {'id': item}}) resp = obj_vol.modify(host_access=host_access) return resp except Exception as e: errormsg = "Failed to detach hosts {0} from volume {1} with error {2} ".format(host_list_detach, obj_vol.name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_volume(self, obj_vol, to_modify_dict): """modify volume attributes :param obj_vol: volume instance :param to_modify_dict: dict containing attributes to be modified. :return: None """ try: if 'io_limit_policy' in to_modify_dict.keys(): to_modify_dict['io_limit_policy'] = self.get_io_limit_policy( id=to_modify_dict['io_limit_policy']) if 'snap_schedule' in to_modify_dict.keys() and \ to_modify_dict['snap_schedule'] != "": to_modify_dict['snap_schedule'] = \ {"name": to_modify_dict['snap_schedule']} param_list = ['name', 'size', 'host_access', 'description', 'sp', 'io_limit_policy', 'tiering_policy', 'snap_schedule', 'is_snap_schedule_paused', 'is_compression', 'is_advanced_dedup_enabled'] for item in param_list: if item not in to_modify_dict.keys(): to_modify_dict.update({item: None}) LOG.debug("Final update dict before modify " "api call: %s", to_modify_dict) obj_vol.modify(name=to_modify_dict['name'], size=to_modify_dict['size'], host_access=to_modify_dict['host_access'], description=to_modify_dict['description'], sp=to_modify_dict['sp'], io_limit_policy=to_modify_dict['io_limit_policy'], tiering_policy=to_modify_dict['tiering_policy'], snap_schedule=to_modify_dict['snap_schedule'], is_snap_schedule_paused=to_modify_dict['is_snap_schedule_paused'], is_compression=to_modify_dict['is_compression'], is_advanced_dedup_enabled=to_modify_dict['is_advanced_dedup_enabled']) except Exception as e: errormsg = "Failed to modify the volume {0} " \ "with error {1}".format(obj_vol.name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def delete_volume(self, vol_id): """Delete volume. :param vol_obj: The object instance of the volume to be deleted """ try: obj_vol = self.get_volume(vol_id=vol_id) obj_vol.delete(force_snap_delete=False) return True except Exception as e: errormsg = "Delete operation of volume id:{0} " \ "failed with error {1}".format(id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_volume_host_access_list(self, obj_vol): """ Get volume host access list :param obj_vol: volume instance :return: host list """ host_list = [] if obj_vol.host_access: for host_access in obj_vol.host_access: host = self.get_host(host_id=host_access.host.id).update() hlu = None for host_lun in host.host_luns: if host_lun.lun.name == obj_vol.name: hlu = host_lun.hlu host_list.append({'name': host_access.host.name, 'id': host_access.host.id, 'accessMask': host_access.access_mask.name, 'hlu': hlu}) return host_list def get_volume_display_attributes(self, obj_vol): """get display volume attributes :param obj_vol: volume instance :return: volume dict to display """ try: obj_vol = obj_vol.update() volume_details = obj_vol._get_properties() volume_details['size_total_with_unit'] = utils. \ convert_size_with_unit(int(volume_details['size_total'])) volume_details.update({'host_access': self.get_volume_host_access_list(obj_vol)}) if obj_vol.snap_schedule: volume_details.update( {'snap_schedule': {'name': obj_vol.snap_schedule.name, 'id': obj_vol.snap_schedule.id}}) if obj_vol.io_limit_policy: volume_details.update( {'io_limit_policy': {'name': obj_vol.io_limit_policy.id, 'id': obj_vol.io_limit_policy.id}}) if obj_vol.pool: volume_details.update({'pool': {'name': obj_vol.pool.name, 'id': obj_vol.pool.id}}) return volume_details except Exception as e: errormsg = "Failed to display the volume {0} with " \ "error {1}".format(obj_vol.name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def validate_input_string(self): """ validates the input string checks if it is empty string """ invalid_string = "" try: no_chk_list = ['snap_schedule', 'description'] for key in self.module.params: val = self.module.params[key] if key not in no_chk_list and isinstance(val, str) \ and val == invalid_string: errmsg = 'Invalid input parameter "" for {0}'.format( key) self.module.fail_json(msg=errmsg) except Exception as e: errormsg = "Failed to validate the module param with " \ "error {0}".format(str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def validate_host_list(self, host_list_input): """ validates the host_list_input value for None and empty """ try: for host_list in host_list_input: if ("host_name" in host_list.keys() and "host_id" in host_list.keys()): if host_list["host_name"] and host_list["host_id"]: errmsg = 'parameters are mutually exclusive: host_name|host_id' self.module.fail_json(msg=errmsg) is_host_details_missing = True for key, value in host_list.items(): if key == "host_name" and not is_none_or_empty_string(value): is_host_details_missing = False elif key == "host_id" and not is_none_or_empty_string(value): is_host_details_missing = False if is_host_details_missing: errmsg = 'Invalid input parameter for {0}'.format(key) self.module.fail_json(msg=errmsg) except Exception as e: errormsg = "Failed to validate the module param with " \ "error {0}".format(str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def resolve_host_mappings(self, hosts): """ This method creates a dictionary of hosts and hlu parameter values :param hosts: host and hlu value passed from input file :return: list of host and hlu dictionary """ host_list_new = [] if hosts: for item in hosts: host_dict = dict() host_id = None hlu = None if item['host_name']: host = self.get_host(host_name=item['host_name']) if host: host_id = host.id if item['host_id']: host_id = item['host_id'] if item['hlu']: hlu = item['hlu'] host_dict['host_id'] = host_id host_dict['hlu'] = hlu host_list_new.append(host_dict) return host_list_new def perform_module_operation(self): """ Perform different actions on volume module based on parameters passed in the playbook """ self.new_host_list = [] self.overlapping_list = [] vol_name = self.module.params['vol_name'] vol_id = self.module.params['vol_id'] pool_name = self.module.params['pool_name'] pool_id = self.module.params['pool_id'] size = self.module.params['size'] cap_unit = self.module.params['cap_unit'] snap_schedule = self.module.params['snap_schedule'] io_limit_policy = self.module.params['io_limit_policy'] host_name = self.module.params['host_name'] host_id = self.module.params['host_id'] hlu = self.module.params['hlu'] mapping_state = self.module.params['mapping_state'] new_vol_name = self.module.params['new_vol_name'] state = self.module.params['state'] hosts = self.module.params['hosts'] # result is a dictionary to contain end state and volume details changed = False result = dict( changed=False, volume_details={} ) to_modify_dict = None volume_details = None to_modify_host = False self.validate_input_string() if hosts: self.validate_host_list(hosts) if size is not None and size == 0: self.module.fail_json(msg="Size can not be 0 (Zero)") if size and not cap_unit: cap_unit = 'GB' if (cap_unit is not None) and not size: self.module.fail_json(msg="cap_unit can be specified along " "with size") if hlu and (not host_name and not host_id and not hosts): self.module.fail_json(msg="hlu can be specified with " "host_id or host_name") if mapping_state and (not host_name and not host_id and not hosts): self.module.fail_json(msg="mapping_state can be specified" " with host_id or host_name or hosts") obj_vol = self.get_volume(vol_id=vol_id, vol_name=vol_name) if host_name or host_id: if not mapping_state: errmsg = "'mapping_state' is required along with " \ "'host_name' or 'host_id' or 'hosts'" self.module.fail_json(msg=errmsg) host = [{'host_name': host_name, 'host_id': host_id, 'hlu': hlu}] self.new_host_list = self.resolve_host_mappings(host) if hosts: if not mapping_state: errmsg = "'mapping_state' is required along with " \ "'host_name' or 'host_id' or 'hosts'" self.module.fail_json(msg=errmsg) self.new_host_list += self.resolve_host_mappings(hosts) if io_limit_policy: io_limit_policy = self.get_io_limit_policy(name=io_limit_policy) self.param_io_limit_pol_id = io_limit_policy.id if snap_schedule: snap_schedule = self.get_snap_schedule(name=snap_schedule) self.param_snap_schedule_name = snap_schedule.name[0] # this is for removing existing snap_schedule if snap_schedule == "": self.param_snap_schedule_name = snap_schedule if obj_vol: volume_details = obj_vol._get_properties() vol_id = obj_vol.get_id() to_modify_dict = self.volume_modify_required(obj_vol, cap_unit) LOG.debug("Volume Modify Required: %s", to_modify_dict) if obj_vol.host_access: to_modify_host = self.host_access_modify_required( host_access_list=obj_vol.host_access) LOG.debug("Host Modify Required in access: %s", to_modify_host) elif self.new_host_list: to_modify_host = self.host_access_modify_required( host_access_list=obj_vol.host_access) LOG.debug("Host Modify Required: %s", to_modify_host) if state == 'present' and not volume_details: if not vol_name: msg_noname = "volume with id {0} is not found, unable to " \ "create a volume without a valid " \ "vol_name".format(vol_id) self.module.fail_json(msg=msg_noname) if snap_schedule == "": self.module.fail_json(msg="Invalid snap_schedule") if new_vol_name: self.module.fail_json(msg="new_vol_name is not required " "to create a new volume") if not pool_name and not pool_id: self.module.fail_json(msg="pool_id or pool_name is required " "to create new volume") if not size: self.module.fail_json(msg="Size is required to create" " a volume") host_access = None if self.new_host_list: host_access = [] for item in self.new_host_list: if item['hlu']: host_access.append( {'accessMask': utils.HostLUNAccessEnum.PRODUCTION, 'host': {'id': item['host_id']}, 'hlu': item['hlu']}) else: host_access.append( {'accessMask': utils.HostLUNAccessEnum.PRODUCTION, 'host': {'id': item['host_id']}}) size = utils.get_size_in_gb(size, cap_unit) obj_pool = self.get_pool(pool_name=pool_name, pool_id=pool_id) obj_vol = self.create_volume(obj_pool=obj_pool, size=size, host_access=host_access) if obj_vol: LOG.debug("Successfully created volume , %s", obj_vol) vol_id = obj_vol.id volume_details = obj_vol._get_properties() LOG.debug("Got volume id , %s", vol_id) changed = True if state == 'present' and volume_details and to_modify_dict: self.modify_volume(obj_vol=obj_vol, to_modify_dict=to_modify_dict) changed = True if (state == 'present' and volume_details and mapping_state == 'mapped' and to_modify_host): if self.new_host_list: resp = self.multiple_host_map(host_dic_list=self.new_host_list, obj_vol=obj_vol) changed = True if resp else False if (state == 'present' and volume_details and mapping_state == 'unmapped' and to_modify_host): if self.new_host_list: resp = self.multiple_detach(host_list_detach=self.overlapping_list, obj_vol=obj_vol) LOG.info(resp) changed = True if resp else False if state == 'absent' and volume_details: changed = self.delete_volume(vol_id) volume_details = None if state == 'present' and volume_details: volume_details = self.get_volume_display_attributes( obj_vol=obj_vol) result['changed'] = changed result['volume_details'] = volume_details self.module.exit_json(**result) def get_volume_parameters(): """This method provide parameters required for the ansible volume module on Unity""" return dict( vol_name=dict(required=False, type='str'), vol_id=dict(required=False, type='str'), description=dict(required=False, type='str'), pool_name=dict(required=False, type='str'), pool_id=dict(required=False, type='str'), size=dict(required=False, type='int'), cap_unit=dict(required=False, type='str', choices=['GB', 'TB']), is_thin=dict(required=False, type='bool'), compression=dict(required=False, type='bool'), advanced_dedup=dict(required=False, type='bool'), sp=dict(required=False, type='str', choices=['SPA', 'SPB']), io_limit_policy=dict(required=False, type='str'), snap_schedule=dict(required=False, type='str'), host_name=dict(required=False, type='str'), host_id=dict(required=False, type='str'), hosts=dict(required=False, type='list', elements='dict', options=dict( host_id=dict(required=False, type='str'), host_name=dict(required=False, type='str'), hlu=dict(required=False, type='str') )), hlu=dict(required=False, type='int'), mapping_state=dict(required=False, type='str', choices=['mapped', 'unmapped']), new_vol_name=dict(required=False, type='str'), tiering_policy=dict(required=False, type='str', choices=[ 'AUTOTIER_HIGH', 'AUTOTIER', 'HIGHEST', 'LOWEST']), state=dict(required=True, type='str', choices=['present', 'absent']) ) def main(): """ Create Unity volume object and perform action on it based on user input from playbook""" obj = Volume() obj.perform_module_operation() if __name__ == '__main__': main()