#!/usr/bin/python # Copyright: (c) 2021, Dell Technologies # Apache License version 2.0 (see MODULE-LICENSE or http://www.apache.org/licenses/LICENSE-2.0.txt) """ Ansible module for managing Snapshots on Dell Technologies (Dell) PowerFlex""" from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r''' module: snapshot version_added: '1.0.0' short_description: Manage Snapshots on Dell PowerFlex description: - Managing snapshots on PowerFlex Storage System includes creating, getting details, mapping/unmapping to/from SDC, modifying the attributes and deleting snapshot. author: - Akash Shendge (@shenda1) extends_documentation_fragment: - dellemc.powerflex.powerflex options: snapshot_name: description: - The name of the snapshot. - Mandatory for create operation. - Specify either I(snapshot_name) or I(snapshot_id) (but not both) for any operation. type: str snapshot_id: description: - The ID of the Snapshot. type: str vol_name: description: - The name of the volume for which snapshot will be taken. - Specify either I(vol_name) or I(vol_id) while creating snapshot. type: str vol_id: description: - The ID of the volume. type: str read_only: description: - Specifies whether mapping of the created snapshot volume will have read-write access or limited to read-only access. - If C(true), snapshot is created with read-only access. - If C(false), snapshot is created with read-write access. type: bool size: description: - The size of the snapshot. type: int cap_unit: description: - The unit of the volume size. It defaults to C(GB), if not specified. choices: ['GB' , 'TB'] type: str snapshot_new_name: description: - New name of the snapshot. Used to rename the snapshot. type: str allow_multiple_mappings: description: - Specifies whether to allow multiple mappings or not. type: bool desired_retention: description: - The retention value for the Snapshot. - If the desired_retention is not mentioned during creation, snapshot will be created with unlimited retention. - Maximum supported desired retention is 31 days. type: int retention_unit: description: - The unit for retention. It defaults to C(hours), if not specified. choices: [hours, days] type: str sdc: description: - Specifies SDC parameters. type: list elements: dict suboptions: sdc_name: description: - Name of the SDC. - Specify either I(sdc_name), I(sdc_id) or I(sdc_ip). - Mutually exclusive with I(sdc_id) and I(sdc_ip). type: str sdc_id: description: - ID of the SDC. - Specify either I(sdc_name), I(sdc_id) or I(sdc_ip). - Mutually exclusive with I(sdc_name) and I(sdc_ip). type: str sdc_ip: description: - IP of the SDC. - Specify either I(sdc_name), I(sdc_id) or I(sdc_ip). - Mutually exclusive with I(sdc_id) and I(sdc_ip). type: str access_mode: description: - Define the access mode for all mappings of the snapshot. choices: ['READ_WRITE', 'READ_ONLY', 'NO_ACCESS'] type: str bandwidth_limit: description: - Limit of snapshot network bandwidth. - Need to mention in multiple of 1024 Kbps. - To set no limit, 0 is to be passed. type: int iops_limit: description: - Limit of snapshot IOPS. - Minimum IOPS limit is 11 and specify 0 for unlimited iops. type: int sdc_state: description: - Mapping state of the SDC. choices: ['mapped', 'unmapped'] type: str remove_mode: description: - Removal mode for the snapshot. - It defaults to C(ONLY_ME), if not specified. choices: ['ONLY_ME', 'INCLUDING_DESCENDANTS'] type: str state: description: - State of the snapshot. choices: ['present', 'absent'] required: true type: str notes: - The I(check_mode) is not supported. ''' EXAMPLES = r''' - name: Create snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_name: "ansible_snapshot" vol_name: "ansible_volume" read_only: false desired_retention: 2 state: "present" - name: Get snapshot details using snapshot id dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" state: "present" - name: Map snapshot to SDC dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" sdc: - sdc_ip: "198.10.xxx.xxx" - sdc_id: "663ac0d200000001" allow_multiple_mappings: true sdc_state: "mapped" state: "present" - name: Modify the attributes of SDC mapped to snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" sdc: - sdc_ip: "198.10.xxx.xxx" iops_limit: 11 bandwidth_limit: 4096 - sdc_id: "663ac0d200000001" iops_limit: 20 bandwidth_limit: 2048 allow_multiple_mappings: true sdc_state: "mapped" state: "present" - name: Extend the size of snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" size: 16 state: "present" - name: Unmap SDCs from snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" sdc: - sdc_ip: "198.10.xxx.xxx" - sdc_id: "663ac0d200000001" sdc_state: "unmapped" state: "present" - name: Rename snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" snapshot_new_name: "ansible_renamed_snapshot_10" state: "present" - name: Delete snapshot dellemc.powerflex.snapshot: hostname: "{{hostname}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_id: "fe6cb28200000007" remove_mode: "ONLY_ME" state: "absent" ''' RETURN = r''' changed: description: Whether or not the resource has changed. returned: always type: bool sample: 'false' snapshot_details: description: Details of the snapshot. returned: When snapshot exists type: dict contains: ancestorVolumeId: description: The ID of the root of the specified volume's V-Tree. type: str ancestorVolumeName: description: The name of the root of the specified volume's V-Tree. type: str creationTime: description: The creation time of the snapshot. type: int id: description: The ID of the snapshot. type: str mappedSdcInfo: description: The details of the mapped SDC. type: dict contains: sdcId: description: ID of the SDC. type: str sdcName: description: Name of the SDC. type: str sdcIp: description: IP of the SDC. type: str accessMode: description: Mapping access mode for the specified snapshot. type: str limitIops: description: IOPS limit for the SDC. type: int limitBwInMbps: description: Bandwidth limit for the SDC. type: int name: description: Name of the snapshot. type: str secureSnapshotExpTime: description: Expiry time of the snapshot. type: int sizeInKb: description: Size of the snapshot. type: int sizeInGb: description: Size of the snapshot. type: int retentionInHours: description: Retention of the snapshot in hours. type: int storagePoolId: description: The ID of the Storage pool in which snapshot resides. type: str storagePoolName: description: The name of the Storage pool in which snapshot resides. type: str sample: { "accessModeLimit": "ReadOnly", "ancestorVolumeId": "cdd883cf00000002", "ancestorVolumeName": "ansible-volume-1", "autoSnapshotGroupId": null, "compressionMethod": "Invalid", "consistencyGroupId": "22f1e80c00000001", "creationTime": 1631619229, "dataLayout": "MediumGranularity", "id": "cdd883d000000004", "links": [ { "href": "/api/instances/Volume::cdd883d000000004", "rel": "self" }, { "href": "/api/instances/Volume::cdd883d000000004/relationships /Statistics", "rel": "/api/Volume/relationship/Statistics" }, { "href": "/api/instances/Volume::cdd883cf00000002", "rel": "/api/parent/relationship/ancestorVolumeId" }, { "href": "/api/instances/VTree::6e86255c00000001", "rel": "/api/parent/relationship/vtreeId" }, { "href": "/api/instances/StoragePool::e0d8f6c900000000", "rel": "/api/parent/relationship/storagePoolId" } ], "lockedAutoSnapshot": false, "lockedAutoSnapshotMarkedForRemoval": false, "managedBy": "ScaleIO", "mappedSdcInfo": null, "name": "ansible_vol_snap_1", "notGenuineSnapshot": false, "originalExpiryTime": 0, "pairIds": null, "replicationJournalVolume": false, "replicationTimeStamp": 0, "retentionInHours": 0, "retentionLevels": [], "secureSnapshotExpTime": 0, "sizeInGb": 16, "sizeInKb": 16777216, "snplIdOfAutoSnapshot": null, "snplIdOfSourceVolume": null, "storagePoolId": "e0d8f6c900000000", "storagePoolName": "pool1", "timeStampIsAccurate": false, "useRmcache": false, "volumeReplicationState": "UnmarkedForReplication", "volumeType": "Snapshot", "vtreeId": "6e86255c00000001" } ''' from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.powerflex.plugins.module_utils.storage.dell\ import utils from datetime import datetime, timedelta import time import copy LOG = utils.get_logger('snapshot') class PowerFlexSnapshot(object): """Class with Snapshot operations""" def __init__(self): """ Define all parameters required by this module""" self.module_params = utils.get_powerflex_gateway_host_parameters() self.module_params.update(get_powerflex_snapshot_parameters()) mutually_exclusive = [['snapshot_name', 'snapshot_id'], ['vol_name', 'vol_id'], ['snapshot_id', 'vol_name'], ['snapshot_id', 'vol_id']] required_together = [['sdc', 'sdc_state']] required_one_of = [['snapshot_name', 'snapshot_id']] # initialize the Ansible module self.module = AnsibleModule( argument_spec=self.module_params, supports_check_mode=False, mutually_exclusive=mutually_exclusive, required_together=required_together, required_one_of=required_one_of) utils.ensure_required_libs(self.module) try: self.powerflex_conn = utils.get_powerflex_gateway_host_connection( self.module.params) LOG.info("Got the PowerFlex system connection object instance") except Exception as e: LOG.error(str(e)) self.module.fail_json(msg=str(e)) def get_storage_pool(self, storage_pool_id): """Get storage pool details :param storage_pool_id: The storage pool id :return: Storage pool details """ try: return self.powerflex_conn.storage_pool.get( filter_fields={'id': storage_pool_id}) except Exception as e: errormsg = "Failed to get the storage pool %s with error " \ "%s" % (storage_pool_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_snapshot(self, snapshot_name=None, snapshot_id=None): """Get snapshot details :param snapshot_name: Name of the snapshot :param snapshot_id: ID of the snapshot :return: Details of snapshot if exist. """ id_or_name = snapshot_id if snapshot_id else snapshot_name try: if snapshot_name: snapshot_details = self.powerflex_conn.volume.get( filter_fields={'name': snapshot_name}) else: snapshot_details = self.powerflex_conn.volume.get( filter_fields={'id': snapshot_id}) if len(snapshot_details) == 0: msg = "Snapshot with identifier %s is not found" % id_or_name LOG.error(msg) return None if len(snapshot_details) > 1: errormsg = "Multiple instances of snapshot " \ "exist with name {0}".format(snapshot_name) self.module.fail_json(msg=errormsg) # Add ancestor volume name if 'ancestorVolumeId' in snapshot_details[0] and \ snapshot_details[0]['ancestorVolumeId']: vol = self.get_volume( vol_id=snapshot_details[0]['ancestorVolumeId']) snapshot_details[0]['ancestorVolumeName'] = vol['name'] # Add size in GB if 'sizeInKb' in snapshot_details[0] and \ snapshot_details[0]['sizeInKb']: snapshot_details[0]['sizeInGb'] = utils.get_size_in_gb( snapshot_details[0]['sizeInKb'], 'KB') # Add storage pool name if 'storagePoolId' in snapshot_details[0] and \ snapshot_details[0]['storagePoolId']: sp = self.get_storage_pool(snapshot_details[0]['storagePoolId']) if len(sp) > 0: snapshot_details[0]['storagePoolName'] = sp[0]['name'] # Add retention in hours if 'secureSnapshotExpTime' in snapshot_details[0] and\ 'creationTime' in snapshot_details[0]: if snapshot_details[0]['secureSnapshotExpTime'] != 0: expiry_obj = datetime.fromtimestamp( snapshot_details[0]['secureSnapshotExpTime']) creation_obj = datetime.fromtimestamp( snapshot_details[0]['creationTime']) td = utils.dateutil.relativedelta.relativedelta( expiry_obj, creation_obj) snapshot_details[0]['retentionInHours'] = td.hours else: snapshot_details[0]['retentionInHours'] = 0 # Match volume details with snapshot details if any([self.module.params['vol_name'], self.module.params['vol_id']]): self.match_vol_details(snapshot_details[0]) return snapshot_details[0] except Exception as e: errormsg = "Failed to get the snapshot %s with error %s" % ( id_or_name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def match_vol_details(self, snapshot): """Match the given volume details with the response :param snapshot: The snapshot details """ vol_name = self.module.params['vol_name'] vol_id = self.module.params['vol_id'] try: if vol_name and vol_name != snapshot['ancestorVolumeName']: errormsg = "Given volume name do not match with the " \ "corresponding snapshot details." self.module.fail_json(msg=errormsg) if vol_id and vol_id != snapshot['ancestorVolumeId']: errormsg = "Given volume ID do not match with the " \ "corresponding snapshot details." self.module.fail_json(msg=errormsg) except Exception as e: errormsg = "Failed to match volume details with the snapshot " \ "with error %s" % str(e) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_volume(self, vol_name=None, vol_id=None): """Get the volume id :param vol_name: The name of the volume :param vol_id: The ID of the volume :return: The volume details """ try: if vol_name: vol_details = self.powerflex_conn.volume.get( filter_fields={'name': vol_name}) else: vol_details = self.powerflex_conn.volume.get( filter_fields={'id': vol_id}) if len(vol_details) == 0: error_msg = "Unable to find volume with name {0}".format( vol_name) self.module.fail_json(msg=error_msg) return vol_details[0] except Exception as e: errormsg = "Failed to get the volume %s with error " \ "%s" % (vol_name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_sdc_id(self, sdc_name=None, sdc_ip=None, sdc_id=None): """Get the SDC ID :param sdc_name: The name of the SDC :param sdc_ip: The IP of the SDC :param sdc_id: The ID of the SDC :return: The ID of the SDC """ if sdc_name: id_ip_name = sdc_name elif sdc_ip: id_ip_name = sdc_ip else: id_ip_name = sdc_id try: if sdc_name: sdc_details = self.powerflex_conn.sdc.get( filter_fields={'name': sdc_name}) elif sdc_ip: sdc_details = self.powerflex_conn.sdc.get( filter_fields={'sdcIp': sdc_ip}) else: sdc_details = self.powerflex_conn.sdc.get( filter_fields={'id': sdc_id}) if len(sdc_details) == 0: error_msg = "Unable to find SDC with identifier {0}".format( id_ip_name) self.module.fail_json(msg=error_msg) return sdc_details[0]['id'] except Exception as e: errormsg = "Failed to get the SDC %s with error " \ "%s" % (id_ip_name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def get_system_id(self): """Get system id""" try: resp = self.powerflex_conn.system.get() if len(resp) == 0: self.module.fail_json(msg="No system exist on the given host.") if len(resp) > 1: self.module.fail_json(msg="Multiple systems exist on the " "given host.") return resp[0]['id'] except Exception as e: msg = "Failed to get system id with error %s" % str(e) LOG.error(msg) self.module.fail_json(msg=msg) def create_snapshot(self, snapshot_name, vol_id, system_id, access_mode, retention): """Create snapshot :param snapshot_name: The name of the snapshot :param vol_id: The ID of the source volume :param system_id: The system id :param access_mode: Access mode for the snapshot :param retention: The retention for the snapshot :return: Boolean indicating if create operation is successful """ LOG.debug("Creating Snapshot") try: self.powerflex_conn.system.snapshot_volumes( system_id=system_id, snapshot_defs=[utils.SnapshotDef(vol_id, snapshot_name)], access_mode=access_mode, retention_period=retention ) return True except Exception as e: errormsg = "Create snapshot %s operation failed with " \ "error %s" % (snapshot_name, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_retention(self, snapshot_id, new_retention): """Modify snapshot retention :param snapshot_id: The snapshot id :param new_retention: Desired retention of the snapshot :return: Boolean indicating if modifying retention is successful """ try: self.powerflex_conn.volume.set_retention_period(snapshot_id, new_retention) return True except Exception as e: errormsg = "Modify retention of snapshot %s operation failed " \ "with error %s" % (snapshot_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_size(self, snapshot_id, new_size): """Modify snapshot size :param snapshot_id: The snapshot id :param new_size: Size of the snapshot :return: Boolean indicating if extend operation is successful """ try: self.powerflex_conn.volume.extend(snapshot_id, new_size) return True except Exception as e: errormsg = "Extend snapshot %s operation failed with " \ "error %s" % (snapshot_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_snap_access_mode(self, snapshot_id, snap_access_mode): """Modify access mode of snapshot :param snapshot_id: The snapshot id :param snap_access_mode: Access mode of the snapshot :return: Boolean indicating if modifying access mode of snapshot is successful """ try: self.powerflex_conn.volume.set_volume_access_mode_limit( volume_id=snapshot_id, access_mode_limit=snap_access_mode) return True except Exception as e: errormsg = "Modify access mode of snapshot %s operation " \ "failed with error %s" % (snapshot_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_access_mode(self, snapshot_id, access_mode_list): """Modify access mode of SDCs mapped to snapshot :param snapshot_id: The snapshot id :param access_mode_list: List containing SDC ID's whose access mode is to modified :return: Boolean indicating if modifying access mode is successful """ try: changed = False for temp in access_mode_list: if temp['accessMode']: self.powerflex_conn.volume.set_access_mode_for_sdc( volume_id=snapshot_id, sdc_id=temp['sdc_id'], access_mode=temp['accessMode']) changed = True return changed except Exception as e: errormsg = "Modify access mode of SDC %s operation failed " \ "with error %s" % (temp['sdc_id'], str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def modify_limits(self, payload): """Modify IOPS and bandwidth limits of SDC's mapped to snapshot :param snapshot_id: The snapshot id :param limits_dict: Dict containing SDC ID's whose bandwidth and IOPS is to modified :return: Boolean indicating if modifying limits is successful """ try: changed = False if payload['bandwidth_limit'] is not None or \ payload['iops_limit'] is not None: self.powerflex_conn.volume.set_mapped_sdc_limits(**payload) changed = True return changed except Exception as e: errormsg = "Modify bandwidth/iops limits of SDC %s operation " \ "failed with error %s" % (payload['sdc_id'], str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def rename_snapshot(self, snapshot_id, new_name): """Rename snapshot :param snapshot_id: The snapshot id :param new_name: The new name of the snapshot :return: Boolean indicating if rename operation is successful """ try: self.powerflex_conn.volume.rename(snapshot_id, new_name) return True except Exception as e: errormsg = "Rename snapshot %s operation failed with " \ "error %s" % (snapshot_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def delete_snapshot(self, snapshot_id, remove_mode): """Delete snapshot :param snapshot_id: The snapshot id :param remove_mode: Removal mode for the snapshot :return: Boolean indicating if delete operation is successful """ try: self.powerflex_conn.volume.delete(snapshot_id, remove_mode) return True except Exception as e: errormsg = "Delete snapshot %s operation failed with " \ "error %s" % (snapshot_id, str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def validate_desired_retention(self, desired_retention, retention_unit): """Validates the specified desired retention. :param desired_retention: Desired retention of the snapshot :param retention_unit: Retention unit for snapshot """ if retention_unit == 'hours' and (desired_retention < 1 or desired_retention > 744): self.module.fail_json(msg="Please provide a valid integer as the" " desired retention between 1 and 744.") elif retention_unit == 'days' and (desired_retention < 1 or desired_retention > 31): self.module.fail_json(msg="Please provide a valid integer as the" " desired retention between 1 and 31.") def unmap_snapshot_from_sdc(self, snapshot, sdc): """Unmap SDC's from snapshot :param snapshot: Snapshot details :param sdc: List of SDCs to be unmapped :return: Boolean indicating if unmap operation is successful """ current_sdcs = snapshot['mappedSdcInfo'] current_sdc_ids = [] sdc_id_list = [] if current_sdcs: for temp in current_sdcs: current_sdc_ids.append(temp['sdcId']) for temp in sdc: if 'sdc_name' in temp and temp['sdc_name']: sdc_id = self.get_sdc_id(sdc_name=temp['sdc_name']) elif 'sdc_ip' in temp and temp['sdc_ip']: sdc_id = self.get_sdc_id(sdc_ip=temp['sdc_ip']) else: sdc_id = self.get_sdc_id(sdc_id=temp['sdc_id']) if sdc_id in current_sdc_ids: sdc_id_list.append(sdc_id) LOG.info("SDC IDs to remove %s", sdc_id_list) if len(sdc_id_list) == 0: return False try: for sdc_id in sdc_id_list: self.powerflex_conn.volume.remove_mapped_sdc( snapshot['id'], sdc_id) return True except Exception as e: errormsg = "Unmap SDC %s from snapshot %s failed with error " \ "%s" % (sdc_id, snapshot['id'], str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def map_snapshot_to_sdc(self, snapshot, sdc): """Map SDC's to snapshot :param snapshot: Snapshot details :param sdc: List of SDCs :return: Boolean indicating if mapping operation is successful """ current_sdcs = snapshot['mappedSdcInfo'] current_sdc_ids = [] sdc_id_list = [] sdc_map_list = [] sdc_modify_list1 = [] sdc_modify_list2 = [] if current_sdcs: for temp in current_sdcs: current_sdc_ids.append(temp['sdcId']) for temp in sdc: if 'sdc_name' in temp and temp['sdc_name']: sdc_id = self.get_sdc_id(sdc_name=temp['sdc_name']) elif 'sdc_ip' in temp and temp['sdc_ip']: sdc_id = self.get_sdc_id(sdc_ip=temp['sdc_ip']) else: sdc_id = self.get_sdc_id(sdc_id=temp['sdc_id']) if sdc_id not in current_sdc_ids: sdc_id_list.append(sdc_id) temp['sdc_id'] = sdc_id if 'access_mode' in temp: temp['access_mode'] = get_access_mode(temp['access_mode']) if 'bandwidth_limit' not in temp: temp['bandwidth_limit'] = None if 'iops_limit' not in temp: temp['iops_limit'] = None sdc_map_list.append(temp) else: access_mode_dict, limits_dict = check_for_sdc_modification( snapshot, sdc_id, temp) if access_mode_dict: sdc_modify_list1.append(access_mode_dict) if limits_dict: sdc_modify_list2.append(limits_dict) LOG.info("SDC to add: %s", sdc_map_list) if not sdc_map_list: return False, sdc_modify_list1, sdc_modify_list2 try: changed = False for sdc in sdc_map_list: payload = { "volume_id": snapshot['id'], "sdc_id": sdc['sdc_id'], "access_mode": sdc['access_mode'], "allow_multiple_mappings": self.module.params['allow_multiple_mappings'] } self.powerflex_conn.volume.add_mapped_sdc(**payload) if sdc['bandwidth_limit'] or sdc['iops_limit']: payload = { "volume_id": snapshot['id'], "sdc_id": sdc['sdc_id'], "bandwidth_limit": sdc['bandwidth_limit'], "iops_limit": sdc['iops_limit'] } self.powerflex_conn.volume.set_mapped_sdc_limits(**payload) changed = True return changed, sdc_modify_list1, sdc_modify_list2 except Exception as e: errormsg = "Mapping snapshot %s to SDC %s " \ "failed with error %s" % (snapshot['name'], sdc['sdc_id'], str(e)) LOG.error(errormsg) self.module.fail_json(msg=errormsg) def validate_parameters(self): """Validate the input parameters""" sdc = self.module.params['sdc'] cap_unit = self.module.params['cap_unit'] size = self.module.params['size'] desired_retention = self.module.params['desired_retention'] retention_unit = self.module.params['retention_unit'] param_list = ['snapshot_name', 'snapshot_id', 'vol_name', 'vol_id'] for param in param_list: if self.module.params[param] is not None and \ len(self.module.params[param].strip()) == 0: error_msg = "Please provide valid %s" % param self.module.fail_json(msg=error_msg) if sdc: for temp in sdc: if (all([temp['sdc_id'], temp['sdc_ip']]) or all([temp['sdc_id'], temp['sdc_name']]) or all([temp['sdc_ip'], temp['sdc_name']])): self.module.fail_json(msg="sdc_id, sdc_ip and sdc_name " "are mutually exclusive") if (cap_unit is not None) and not size: self.module.fail_json(msg="cap_unit can be specified along " "with size") if (retention_unit is not None) and not desired_retention: self.module.fail_json(msg="retention_unit can be specified along " "with desired_retention") def perform_module_operation(self): """ Perform different actions on snapshot based on parameters passed in the playbook """ snapshot_name = self.module.params['snapshot_name'] snapshot_id = self.module.params['snapshot_id'] vol_name = self.module.params['vol_name'] vol_id = self.module.params['vol_id'] read_only = self.module.params['read_only'] size = self.module.params['size'] cap_unit = self.module.params['cap_unit'] snapshot_new_name = self.module.params['snapshot_new_name'] sdc = copy.deepcopy(self.module.params['sdc']) sdc_state = self.module.params['sdc_state'] desired_retention = self.module.params['desired_retention'] retention_unit = self.module.params['retention_unit'] remove_mode = self.module.params['remove_mode'] state = self.module.params['state'] # result is a dictionary to contain end state and snapshot details changed = False is_modified = False result = dict( changed=False, snapshot_details={} ) self.validate_parameters() if size and not cap_unit: cap_unit = 'GB' if desired_retention and not retention_unit: retention_unit = 'hours' if desired_retention is not None: self.validate_desired_retention(desired_retention, retention_unit) snapshot_details = self.get_snapshot(snapshot_name, snapshot_id) if snapshot_details: snap_access_mode = None if read_only is not None: if read_only: snap_access_mode = 'ReadOnly' else: snap_access_mode = 'ReadWrite' is_modified, flag1, flag2, flag3 = check_snapshot_modified( snapshot_details, desired_retention, retention_unit, size, cap_unit, snap_access_mode) if state == 'present' and not snapshot_details: if snapshot_id: self.module.fail_json(msg="Creation of snapshot is allowed " "using snapshot_name only, " "snapshot_id given.") if snapshot_name is None or len(snapshot_name.strip()) == 0: self.module.fail_json(msg="Please provide valid snapshot " "name.") if vol_name is None and vol_id is None: self.module.fail_json(msg="Please provide volume details to " "create new snapshot") if snapshot_new_name is not None: self.module.fail_json(msg="snapshot_new_name is not required" " while creating snapshot") if remove_mode: self.module.fail_json(msg="remove_mode is not required while " "creating snapshot") if vol_name: vol = self.get_volume(vol_name=vol_name) vol_id = vol['id'] retention = 0 if desired_retention: retention = calculate_retention(desired_retention, retention_unit) system_id = self.get_system_id() if read_only: access_mode = 'ReadOnly' else: access_mode = 'ReadWrite' changed = self.create_snapshot(snapshot_name, vol_id, system_id, access_mode, retention) if changed: snapshot_details = self.get_snapshot(snapshot_name) if size: if cap_unit == 'GB': new_size = size * 1024 * 1024 else: new_size = size * 1024 * 1024 * 1024 if new_size != snapshot_details['sizeInKb']: if cap_unit == 'TB': size = size * 1024 changed = self.modify_size(snapshot_details['id'], size) if is_modified: if flag1: retention = calculate_retention(desired_retention, retention_unit) changed = self.modify_retention(snapshot_details['id'], retention) if flag2: new_size = size if cap_unit == 'TB': new_size = size * 1024 changed = self.modify_size(snapshot_details['id'], new_size) if flag3: changed = self.modify_snap_access_mode( snapshot_details['id'], snap_access_mode) if state == 'present' and snapshot_details and sdc and \ sdc_state == 'mapped': changed_mode = False changed_limits = False changed, access_mode_list, limits_list = \ self.map_snapshot_to_sdc(snapshot_details, sdc) if len(access_mode_list) > 0: changed_mode = self.modify_access_mode( snapshot_details['id'], access_mode_list) if len(limits_list) > 0: for temp in limits_list: payload = { "volume_id": snapshot_details['id'], "sdc_id": temp['sdc_id'], "bandwidth_limit": temp['bandwidth_limit'], "iops_limit": temp['iops_limit'] } changed_limits = self.modify_limits(payload) if changed_mode or changed_limits: changed = True if state == 'present' and snapshot_details and sdc and \ sdc_state == 'unmapped': changed = self.unmap_snapshot_from_sdc(snapshot_details, sdc) if state == 'present' and snapshot_details and \ snapshot_new_name is not None: if len(snapshot_new_name.strip()) == 0: self.module.fail_json(msg="Please provide valid snapshot " "name.") changed = self.rename_snapshot(snapshot_details['id'], snapshot_new_name) if changed: snapshot_name = snapshot_new_name if state == 'absent' and snapshot_details: if remove_mode is None: remove_mode = "ONLY_ME" changed = self.delete_snapshot(snapshot_details['id'], remove_mode) if state == 'present': snapshot_details = self.get_snapshot(snapshot_name, snapshot_id) result['snapshot_details'] = snapshot_details result['changed'] = changed self.module.exit_json(**result) def check_snapshot_modified(snapshot=None, desired_retention=None, retention_unit=None, size=None, cap_unit=None, access_mode=None): """Check if snapshot modification is required :param snapshot: Snapshot details :param desired_retention: Desired retention of the snapshot :param retention_unit: Retention unit for snapshot :param size: Size of the snapshot :param cap_unit: Capacity unit for the snapshot :param access_mode: Access mode of the snapshot :return: Boolean indicating if modification is needed """ snap_creation_timestamp = None expiration_timestamp = None is_timestamp_modified = False is_size_modified = False is_access_modified = False is_modified = False if 'creationTime' in snapshot: snap_creation_timestamp = snapshot['creationTime'] if desired_retention: if retention_unit == 'hours': expiration_timestamp = \ datetime.fromtimestamp(snap_creation_timestamp) + \ timedelta(hours=desired_retention) expiration_timestamp = time.mktime(expiration_timestamp.timetuple()) else: expiration_timestamp = \ datetime.fromtimestamp(snap_creation_timestamp) + \ timedelta(days=desired_retention) expiration_timestamp = time.mktime(expiration_timestamp.timetuple()) if 'secureSnapshotExpTime' in snapshot and expiration_timestamp and \ snapshot['secureSnapshotExpTime'] != expiration_timestamp: existing_timestamp = snapshot['secureSnapshotExpTime'] new_timestamp = expiration_timestamp info_message = 'The existing timestamp is: %s and the new ' \ 'timestamp is: %s' % (existing_timestamp, new_timestamp) LOG.info(info_message) existing_time_obj = datetime.fromtimestamp(existing_timestamp) new_time_obj = datetime.fromtimestamp(new_timestamp) if existing_time_obj > new_time_obj: td = utils.dateutil.relativedelta.relativedelta( existing_time_obj, new_time_obj) else: td = utils.dateutil.relativedelta.relativedelta( new_time_obj, existing_time_obj) LOG.info("Time difference: %s", td.minutes) # A delta of two minutes is treated as idempotent if td.seconds > 120 or td.minutes > 2: is_timestamp_modified = True if size: if cap_unit == 'GB': new_size = size * 1024 * 1024 else: new_size = size * 1024 * 1024 * 1024 if new_size != snapshot['sizeInKb']: is_size_modified = True if access_mode and snapshot['accessModeLimit'] != access_mode: is_access_modified = True if is_timestamp_modified or is_size_modified or is_access_modified: is_modified = True return is_modified, is_timestamp_modified, is_size_modified, is_access_modified def calculate_retention(desired_retention=None, retention_unit=None): """ :param desired_retention: Desired retention of the snapshot :param retention_unit: Retention unit for snapshot :return: Retention in minutes """ retention = 0 if retention_unit == 'days': retention = desired_retention * 24 * 60 else: retention = desired_retention * 60 return retention def check_for_sdc_modification(snapshot, sdc_id, sdc_details): """ :param snapshot: The snapshot details :param sdc_id: The ID of the SDC :param sdc_details: The details of SDC :return: Dictionary with SDC attributes to be modified """ access_mode_dict = dict() limits_dict = dict() for sdc in snapshot['mappedSdcInfo']: if sdc['sdcId'] == sdc_id: if sdc['accessMode'] != get_access_mode(sdc_details['access_mode']): access_mode_dict['sdc_id'] = sdc_id access_mode_dict['accessMode'] = get_access_mode( sdc_details['access_mode']) if sdc['limitIops'] != sdc_details['iops_limit'] or \ sdc['limitBwInMbps'] != sdc_details['bandwidth_limit']: limits_dict['sdc_id'] = sdc_id limits_dict['iops_limit'] = None limits_dict['bandwidth_limit'] = None if sdc['limitIops'] != sdc_details['iops_limit']: limits_dict['iops_limit'] = sdc_details['iops_limit'] if sdc['limitBwInMbps'] != get_limits_in_mb(sdc_details['bandwidth_limit']): limits_dict['bandwidth_limit'] = \ sdc_details['bandwidth_limit'] break return access_mode_dict, limits_dict def get_limits_in_mb(limits): """ :param limits: Limits in KB :return: Limits in MB """ if limits: return limits / 1024 def get_access_mode(access_mode): """ :param access_mode: Access mode of the SDC :return: The enum for the access mode """ access_mode_dict = { "READ_WRITE": "ReadWrite", "READ_ONLY": "ReadOnly", "NO_ACCESS": "NoAccess" } return access_mode_dict.get(access_mode) def get_powerflex_snapshot_parameters(): """This method provide parameter required for the Ansible snapshot module on PowerFlex""" return dict( snapshot_name=dict(), snapshot_id=dict(), vol_name=dict(), vol_id=dict(), read_only=dict(required=False, type='bool'), size=dict(required=False, type='int'), cap_unit=dict(choices=['GB', 'TB']), snapshot_new_name=dict(), allow_multiple_mappings=dict(required=False, type='bool'), sdc=dict( type='list', elements='dict', options=dict( sdc_id=dict(), sdc_ip=dict(), sdc_name=dict(), access_mode=dict(choices=['READ_WRITE', 'READ_ONLY', 'NO_ACCESS']), bandwidth_limit=dict(type='int'), iops_limit=dict(type='int') ) ), desired_retention=dict(type='int'), retention_unit=dict(choices=['hours', 'days']), remove_mode=dict(choices=['ONLY_ME', 'INCLUDING_DESCENDANTS']), sdc_state=dict(choices=['mapped', 'unmapped']), state=dict(required=True, type='str', choices=['present', 'absent']) ) def main(): """ Create PowerFlex Snapshot object and perform actions on it based on user input from playbook""" obj = PowerFlexSnapshot() obj.perform_module_operation() if __name__ == '__main__': main()