#!/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 Snapshots on Unity""" from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r''' --- module: snapshot short_description: Manage snapshots on the Unity storage system description: - Managing snapshots on the Unity storage system includes create snapshot, delete snapshot, update snapshot, get snapshot, map host and unmap host. version_added: '1.1.0' extends_documentation_fragment: - dellemc.unity.unity author: - P Srinivas Rao (@srinivas-rao5) options: snapshot_name: description: - The name of the snapshot. - Mandatory parameter for creating a snapshot. - For all other operations either I(snapshot_name) or I(snapshot_id) is required. type: str vol_name: description: - The name of the volume for which snapshot is created. - For creation of a snapshot either I(vol_name) or I(cg_name) is required. - Not required for other operations. type: str cg_name: description: - The name of the Consistency Group for which snapshot is created. - For creation of a snapshot either I(vol_name) or I(cg_name) is required. - Not required for other operations. type: str snapshot_id: description: - The id of the snapshot. - For all operations other than creation either I(snapshot_name) or I(snapshot_id) is required. type: str auto_delete: description: - This option specifies whether the snapshot is auto deleted or not. - If set to C(true), snapshot will expire based on the pool auto deletion policy. - If set to (false), snapshot will not be auto deleted based on the pool auto deletion policy. - Option I(auto_delete) can not be set to C(true), if I(expiry_time) is specified. - If during creation neither I(auto_delete) nor I(expiry_time) is mentioned then snapshot will be created keeping I(auto_delete) as C(true). - Once the I(expiry_time) is set then snapshot cannot be assigned to the auto delete policy. type: bool expiry_time: description: - This option is for specifying the date and time after which the snapshot will expire. - The time is to be mentioned in UTC timezone. - The format is "MM/DD/YYYY HH:MM". Year must be in 4 digits. type: str description: description: - The additional information about the snapshot can be provided using this option. type: str new_snapshot_name: description: - New name for the snapshot. type: str state: description: - The I(state) option is used to mention the existence of the snapshot. type: str required: true choices: [ 'absent', 'present' ] host_name: description: - The name of the host. - Either I(host_name) or I(host_id) is required to map or unmap a snapshot from a host. - Snapshot can be attached to multiple hosts. type: str host_id: description: - The id of the host. - Either I(host_name) or I(host_id) is required to map or unmap a snapshot from a host. - Snapshot can be attached to multiple hosts. type: str host_state: description: - The I(host_state) option is used to mention the existence of the host for snapshot. - It is required when a snapshot is mapped or unmapped from host. type: str choices: ['mapped', 'unmapped'] notes: - The I(check_mode) is not supported. ''' EXAMPLES = r''' - name: Create a Snapshot for a CG dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" port: "{{port}}" cg_name: "{{cg_name}}" snapshot_name: "{{cg_snapshot_name}}" description: "{{description}}" auto_delete: false state: "present" - name: Create a Snapshot for a volume with Host attached dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" port: "{{port}}" vol_name: "{{vol_name}}" snapshot_name: "{{vol_snapshot_name}}" description: "{{description}}" expiry_time: "04/15/2025 16:30" host_name: "{{host_name}}" host_state: "mapped" state: "present" - name: Unmap a host for a Snapshot dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" port: "{{port}}" snapshot_name: "{{vol_snapshot_name}}" host_name: "{{host_name}}" host_state: "unmapped" state: "present" - name: Map snapshot to a host dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" port: "{{port}}" snapshot_name: "{{vol_snapshot_name}}" host_name: "{{host_name}}" host_state: "mapped" state: "present" - name: Update attributes of a Snapshot for a volume dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_name: "{{vol_snapshot_name}}" new_snapshot_name: "{{new_snapshot_name}}" description: "{{new_description}}" host_name: "{{host_name}}" host_state: "unmapped" state: "present" - name: Delete Snapshot of CG dellemc.unity.snapshot: unispherehost: "{{unispherehost}}" username: "{{username}}" password: "{{password}}" validate_certs: "{{validate_certs}}" snapshot_name: "{{cg_snapshot_name}}" state: "absent" ''' RETURN = r''' changed: description: Whether or not the resource has changed. returned: always type: bool sample: True snapshot_details: description: Details of the snapshot. returned: When snapshot exists type: dict contains: is_auto_delete: description: Additional information mentioned for snapshot. type: str expiration_time: description: Date and time after which the snapshot will expire. type: str hosts_list: description: Contains the name and id of the associated hosts. type: dict id: description: Unique identifier of the snapshot instance. type: str name: description: The name of the snapshot. type: str storage_resource_name: description: Name of the storage resource for which the snapshot exists. type: str storage_resource_id: description: Id of the storage resource for which the snapshot exists. type: str sample: { "access_type": null, "attached_wwn": null, "creation_time": "2022-10-21 08:20:25.803000+00:00", "creator_schedule": null, "creator_type": "SnapCreatorTypeEnum.USER_CUSTOM", "creator_user": { "id": "user_admin" }, "description": "Test snap creation", "existed": true, "expiration_time": null, "hash": 8756689457056, "hosts_list": [], "id": "85899355291", "io_limit_policy": null, "is_auto_delete": true, "is_modifiable": false, "is_modified": false, "is_read_only": true, "is_system_snap": false, "last_writable_time": null, "lun": null, "name": "ansible_snap_cg_1_1", "parent_snap": null, "size": null, "snap_group": null, "state": "SnapStateEnum.READY", "storage_resource_id": "res_95", "storage_resource_name": "CG_ansible_test_2_new" } ''' from ansible.module_utils.basic import AnsibleModule from ansible_collections.dellemc.unity.plugins.module_utils.storage.dell \ import utils from datetime import datetime LOG = utils.get_logger('snapshot') application_type = "Ansible/1.7.1" class Snapshot(object): """Class with Snapshot operations""" def __init__(self): """ Define all parameters required by this module""" self.module_params = utils.get_unity_management_host_parameters() self.module_params.update(get_snapshot_parameters()) mutually_exclusive = [['snapshot_name', 'snapshot_id'], ['vol_name', 'cg_name'], ['host_name', 'host_id']] 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_one_of=required_one_of) utils.ensure_required_libs(self.module) # result is a dictionary that contains changed status and # snapshot details self.result = {"changed": False, 'snapshot_details': {}} self.unity_conn = utils.get_unity_unisphere_connection( self.module.params, application_type) self.snap_obj = utils.snap.UnitySnap(self.unity_conn) LOG.info('Connection established with the Unity Array') def validate_expiry_time(self, expiry_time): """Validates the specified expiry_time""" try: datetime.strptime(expiry_time, '%m/%d/%Y %H:%M') except ValueError: error_msg = "expiry_time not in MM/DD/YYYY HH:MM format" LOG.error(error_msg) self.module.fail_json(msg=error_msg) def to_update(self, snapshot, new_name=None, description=None, auto_del=None, expiry_time=None, host=None, host_state=None): """Determines whether to update the snapshot or not""" # If the snapshot has is_auto_delete True, # Check if auto_delete in the input is either None or True if expiry_time and snapshot.is_auto_delete and \ (auto_del is None or auto_del): self.module.fail_json(msg="expiry_time can be assigned " "when auto delete is False") if auto_del and snapshot.expiration_time: error_msg = "expiry_time for snapshot is set." \ " Once it is set then snapshot cannot" \ " be assigned to auto_delete policy" self.module.fail_json(msg=error_msg) if new_name and new_name != snapshot.name: return True if description and description != snapshot.description: return True if auto_del and auto_del != snapshot.is_auto_delete: return True if to_update_expiry_time(snapshot, expiry_time): return True if host and to_update_host_list(snapshot, host, host_state): return True return False def update_snapshot(self, snapshot, new_name=None, description=None, auto_del=None, expiry_time=None, host_access_list=None): try: duration = None if expiry_time: duration = convert_timestamp_to_sec( expiry_time, self.unity_conn.system_time) if duration and duration <= 0: self.module.fail_json(msg="expiry_time should be after" " the current system time") snapshot.modify(name=new_name, retentionDuration=duration, isAutoDelete=auto_del, description=description, hostAccess=host_access_list) snapshot.update() except Exception as e: error_msg = "Failed to modify snapshot" \ " [name: %s , id: %s] with error %s"\ % (snapshot.name, snapshot.id, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def create_snapshot(self, snap_name, storage_id, description=None, auto_del=None, expiry_time=None): try: duration = None if expiry_time: duration = convert_timestamp_to_sec( expiry_time, self.unity_conn.system_time) if duration <= 0: self.module.fail_json(msg="expiry_time should be after" " the current system time") snapshot = self.snap_obj.create( cli=self.unity_conn._cli, storage_resource=storage_id, name=snap_name, description=description, is_auto_delete=auto_del, retention_duration=duration) return snapshot except Exception as e: error_msg = "Failed to create snapshot" \ " %s with error %s" % (snap_name, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def delete_snapshot(self, snapshot): try: if not bool(get_hosts_dict(snapshot)): snapshot.detach_from(None) snapshot.delete() else: snapshot.delete() return None except Exception as e: error_msg = "Failed to delete snapshot" \ " [name: %s, id: %s] with error %s" \ % (snapshot.name, snapshot.id, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def get_snapshot_obj(self, name=None, id=None): snapshot = id if id else name msg = "Failed to get details of snapshot %s with error %s " try: return self.unity_conn.get_snap(name=name, _id=id) except utils.HttpError as e: if e.http_status == 401: cred_err = "Incorrect username or password , {0}".format( e.message) self.module.fail_json(msg=cred_err) else: err_msg = msg % (snapshot, str(e)) LOG.error(err_msg) self.module.fail_json(msg=err_msg) except utils.UnityResourceNotFoundError as e: err_msg = msg % (snapshot, str(e)) LOG.error(err_msg) return None except Exception as e: err_msg = msg % (snapshot, str(e)) LOG.error(err_msg) self.module.fail_json(msg=err_msg) def get_volume_obj(self, name): try: return self.unity_conn.get_lun(name=name) except Exception as e: error_msg = "Failed to get volume %s with error %s"\ % (name, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def get_cg_obj(self, name): try: return self.unity_conn.get_cg(name=name) except Exception as e: error_msg = "Failed to get cg %s with error %s" % (name, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def get_host_obj(self, name=None, id=None): """ Get the Host object""" try: return self.unity_conn.get_host(name=name, _id=id) except Exception as e: host = id if id else name error_msg = "Failed to get host %s with error %s"\ % (host, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def attach_to_snap(self, snapshot, host): """ Attach snapshot to a host """ try: if not get_hosts_dict(snapshot): snapshot.detach_from(None) snapshot.attach_to(host) snapshot.update() except Exception as e: error_msg = "Failed to attach snapshot [name: %s, id: %s]" \ " to host [%s, %s] with error %s"\ % (snapshot.name, snapshot.id, host.name, host.id, str(e)) LOG.error(error_msg) self.module.fail_json(msg=error_msg) def perform_module_operation(self): """ Perform different actions on snapshot module based on parameters chosen in playbook """ snapshot_name = self.module.params['snapshot_name'] snapshot_id = self.module.params['snapshot_id'] vol_name = self.module.params['vol_name'] cg_name = self.module.params['cg_name'] auto_delete = self.module.params['auto_delete'] expiry_time = self.module.params['expiry_time'] description = self.module.params['description'] new_snapshot_name = self.module.params['new_snapshot_name'] host_name = self.module.params['host_name'] host_id = self.module.params['host_id'] host_state = self.module.params['host_state'] state = self.module.params['state'] host = None storage_resource = None changed = False LOG.info("Getting Snapshot details") snapshot = self.get_snapshot_obj(name=snapshot_name, id=snapshot_id) if snapshot and not snapshot.existed: snapshot = None msg = "snapshot details: %s" % str(snapshot) LOG.info(msg) # Get Volume Object if vol_name is not None: if vol_name == "" or vol_name.isspace(): self.module.fail_json(msg="Invalid vol_name given, Please" " provide a valid vol_name") storage_resource = self.get_volume_obj(name=vol_name) # Get Consistency Group Object if cg_name is not None: if cg_name == "" or cg_name.isspace(): self.module.fail_json(msg="Invalid cg_name given, Please" " provide a valid cg_name") storage_resource = self.get_cg_obj(name=cg_name) # Get host object for volume snapshots if host_id or host_name: if cg_name: self.module.fail_json(msg="Mapping CG snapshot to host" " is not supported.") host = self.get_host_obj(name=host_name, id=host_id) # Check whether host_name or host_id is given in input # along with host_state if (host and not host_state) or (not host and host_state): self.module.fail_json( msg="Either host_name or host_id along with host_state " "is required to map or unmap a snapshot from a host") # Check for error, if user tries to create a snapshot with the # same name for other storage resource. if snapshot and storage_resource and\ (snapshot.storage_resource.id != storage_resource.id): self.module.fail_json( msg="Snapshot %s is of %s storage resource. Cannot create new" " snapshot with same name for %s storage resource" % (snapshot.name, snapshot.storage_resource.name, storage_resource.name)) # check for valid expiry_time if expiry_time is not None and \ (expiry_time == "" or expiry_time.isspace()): self.module.fail_json(msg="Please provide valid expiry_time," " empty expiry_time given") # Check if in input auto_delete is True and expiry_time is not None if expiry_time and auto_delete: error_msg = "Cannot set expiry_time if auto_delete given as True" LOG.info(error_msg) self.module.fail_json(msg=error_msg) # Check whether to modify the snapshot or not update_flag = False if snapshot: update_flag = self.to_update(snapshot, new_name=new_snapshot_name, description=description, auto_del=auto_delete, expiry_time=expiry_time, host=host, host_state=host_state) msg = "update_flag for snapshot %s" % str(update_flag) LOG.info(msg) # Create a Snapshot if not snapshot and state == "present": LOG.info("Creating a snapshot") if snapshot_id: self.module.fail_json(msg="Creation of Snapshot is allowed" " using snapshot_name only, " "snapshot_id given") if snapshot_name == "" or snapshot_name.isspace(): self.module.fail_json(msg="snapshot_name is required for" " creation of a snapshot," " empty snapshot_name given") if not storage_resource: self.module.fail_json(msg="vol_name or cg_name required to" " create a snapshot") if new_snapshot_name: self.module.fail_json( msg="new_snapshot_name can not be assigned" " during creation of a snapshot") snapshot = self.create_snapshot(snapshot_name, storage_resource.id, description, auto_delete, expiry_time) if host and host_state == "mapped": self.attach_to_snap(snapshot, host) changed = True # Update the Snapshot if snapshot and state == "present" and update_flag: LOG.info("Updating the Snapshot details") if host_state == 'mapped': self.attach_to_snap(snapshot, host) self.update_snapshot( snapshot, new_name=new_snapshot_name, description=description, auto_del=auto_delete, expiry_time=expiry_time) elif host_state == 'unmapped': host_access_list = create_host_access_list(snapshot, host, host_state) self.update_snapshot( snapshot, new_name=new_snapshot_name, description=description, auto_del=auto_delete, expiry_time=expiry_time, host_access_list=host_access_list) else: self.update_snapshot( snapshot, new_name=new_snapshot_name, description=description, auto_del=auto_delete, expiry_time=expiry_time) changed = True # Delete the Snapshot if state == "absent" and snapshot: snapshot = self.delete_snapshot(snapshot) changed = True # Add snapshot details to the result. if snapshot: snapshot.update() self.result["snapshot_details"] = \ create_snapshot_details_dict(snapshot) else: self.result["snapshot_details"] = {} self.result["changed"] = changed self.module.exit_json(**self.result) def create_snapshot_details_dict(snapshot): """ Add name and id of storage resource and hosts to snapshot details """ snapshot_dict = snapshot._get_properties() del snapshot_dict['storage_resource'] del snapshot_dict['host_access'] snapshot_dict['hosts_list'] = get_hosts_list( get_hosts_dict(snapshot)) snapshot_dict['storage_resource_name'] = \ snapshot.storage_resource.name snapshot_dict['storage_resource_id'] = \ snapshot.storage_resource.id return snapshot_dict def get_hosts_list(hosts_dict): """ Get the host name and host id of all the associated hosts """ hosts_list = [] if not hosts_dict: return hosts_list for host in list(hosts_dict.keys()): hosts_list.append( { "host_name": host.name, "host_id": host.id } ) return hosts_list def create_host_access_list(snapshot, host, host_state): """ This method creates a List of dictionaries which will be used to modify the list of hosts mapped to a snapshot """ host_access_list = [] hosts_dict = get_hosts_dict(snapshot) # If snapshot is not attached to any host. if not hosts_dict: return None if to_update_host_list(snapshot, host, host_state): if host_state == "mapped": return None for snap_host in list(hosts_dict.keys()): if snap_host != host: access_dict = {'host': snap_host, 'allowedAccess': hosts_dict[snap_host]} host_access_list.append(access_dict) return host_access_list def get_hosts_dict(snapshot): """ This method creates a dictionary, with host as key and allowed access as value """ hosts_dict = {} LOG.info("Inside get_hosts_dict") if not snapshot.host_access: return hosts_dict for host_access_obj in snapshot.host_access: hosts_dict[host_access_obj.host] = \ host_access_obj.allowed_access return hosts_dict def to_update_host_list(snapshot, host, host_state): """ Determines whether to update hosts list or not""" hosts_dict = get_hosts_dict(snapshot) if (not hosts_dict or host not in list(hosts_dict.keys()))\ and host_state == "mapped": return True if (hosts_dict and host in list(hosts_dict.keys())) \ and host_state == "unmapped": return True return False def to_update_expiry_time(snapshot, expiry_time=None): """ Check whether to update expiry_time or not""" if not expiry_time: return False if snapshot.expiration_time is None: return True if convert_timestamp_to_sec(expiry_time, snapshot.expiration_time) != 0: return True return False def convert_timestamp_to_sec(expiry_time, snap_time): """Converts the time difference to seconds""" snap_time_str = snap_time.strftime('%m/%d/%Y %H:%M') snap_timestamp = datetime.strptime(snap_time_str, '%m/%d/%Y %H:%M') expiry_timestamp = datetime.strptime(expiry_time, "%m/%d/%Y %H:%M") return int((expiry_timestamp - snap_timestamp).total_seconds()) def get_snapshot_parameters(): """This method provide parameter required for the ansible snapshot module on Unity""" return dict( snapshot_name=dict(required=False, type='str'), snapshot_id=dict(required=False, type='str'), vol_name=dict(required=False, type='str'), cg_name=dict(required=False, type='str'), auto_delete=dict(required=False, type='bool'), expiry_time=dict(required=False, type='str'), description=dict(required=False, type='str'), new_snapshot_name=dict(required=False, type='str'), host_name=dict(required=False, type='str'), host_id=dict(required=False, type='str'), host_state=dict(required=False, type='str', choices=['mapped', 'unmapped']), state=dict(required=True, type='str', choices=['present', 'absent']) ) def main(): """ Create Unity Snapshot object and perform actions on it based on user input from playbook""" obj = Snapshot() obj.perform_module_operation() if __name__ == '__main__': main()