File: //lib/python3.9/site-packages/ansible_collections/dellemc/unity/plugins/modules/snapshot.py
# 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) <[email protected]>
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"
}
'''
import logging
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.6.0"
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()