#!/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()
