summaryrefslogtreecommitdiffstats
path: root/ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py')
-rw-r--r--ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py1285
1 files changed, 1285 insertions, 0 deletions
diff --git a/ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py b/ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py
new file mode 100644
index 00000000..69caea07
--- /dev/null
+++ b/ansible_collections/dellemc/powerflex/plugins/modules/snapshot.py
@@ -0,0 +1,1285 @@
+#!/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) <ansible.team@dell.com>
+
+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()